diff options
189 files changed, 9835 insertions, 3077 deletions
diff --git a/Android.mk b/Android.mk index 87f6b84a53..ecd36fafa9 100644 --- a/Android.mk +++ b/Android.mk @@ -27,7 +27,7 @@ include $(art_path)/tools/veridex/Android.mk include $(art_path)/build/Android.common_path.mk .PHONY: clean-oat -clean-oat: clean-oat-host clean-oat-target +clean-oat: clean-oat-host .PHONY: clean-oat-host clean-oat-host: @@ -35,21 +35,6 @@ clean-oat-host: rm -rf $(TMPDIR)/*/test-*/dalvik-cache/* rm -rf $(TMPDIR)/android-data/dalvik-cache/* -.PHONY: clean-oat-target -clean-oat-target: - $(ADB) root - $(ADB) wait-for-device remount - $(ADB) shell rm -rf $(ART_TARGET_NATIVETEST_DIR) - $(ADB) shell rm -rf $(ART_TARGET_TEST_DIR) - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/*/* - $(ADB) shell rm -rf $(ART_DEXPREOPT_BOOT_JAR_DIR)/$(DEX2OAT_TARGET_ARCH) - $(ADB) shell rm -rf system/app/$(DEX2OAT_TARGET_ARCH) -ifdef TARGET_2ND_ARCH - $(ADB) shell rm -rf $(ART_DEXPREOPT_BOOT_JAR_DIR)/$($(TARGET_2ND_ARCH_VAR_PREFIX)DEX2OAT_TARGET_ARCH) - $(ADB) shell rm -rf system/app/$($(TARGET_2ND_ARCH_VAR_PREFIX)DEX2OAT_TARGET_ARCH) -endif - $(ADB) shell rm -rf data/run-test/test-*/dalvik-cache/* - ######################################################################## # cpplint rules to style check art source files @@ -116,17 +101,6 @@ include $(art_path)/test/Android.run-test.mk TEST_ART_TARGET_SYNC_DEPS += $(ART_TEST_TARGET_GTEST_DEPENDENCIES) $(ART_TEST_TARGET_RUN_TEST_DEPENDENCIES) -# Make sure /system is writable on the device. -TEST_ART_ADB_ROOT_AND_REMOUNT := \ - ($(ADB) root && \ - $(ADB) wait-for-device remount && \ - (($(ADB) shell touch /system/testfile && \ - ($(ADB) shell rm /system/testfile || true)) || \ - ($(ADB) disable-verity && \ - $(ADB) reboot && \ - $(ADB) wait-for-device root && \ - $(ADB) wait-for-device remount))) - # "mm test-art" to build and run all tests on host and device .PHONY: test-art test-art: @@ -637,96 +611,6 @@ build-art-target-run-tests: build-art-target \ build-art-target-tests: build-art-target-gtests build-art-target-run-tests ######################################################################## -# targets to switch back and forth from libdvm to libart - -.PHONY: use-art -use-art: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libart.so - $(ADB) shell start - -.PHONY: use-artd -use-artd: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libartd.so - $(ADB) shell start - -.PHONY: use-dalvik -use-dalvik: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libdvm.so - $(ADB) shell start - -.PHONY: use-art-full -use-art-full: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/* - $(ADB) shell setprop dalvik.vm.dex2oat-filter \"\" - $(ADB) shell setprop dalvik.vm.image-dex2oat-filter \"\" - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libart.so - $(ADB) shell setprop dalvik.vm.usejit false - $(ADB) shell start - -.PHONY: use-artd-full -use-artd-full: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/* - $(ADB) shell setprop dalvik.vm.dex2oat-filter \"\" - $(ADB) shell setprop dalvik.vm.image-dex2oat-filter \"\" - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libartd.so - $(ADB) shell setprop dalvik.vm.usejit false - $(ADB) shell start - -.PHONY: use-art-jit -use-art-jit: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/* - $(ADB) shell setprop dalvik.vm.dex2oat-filter "verify-at-runtime" - $(ADB) shell setprop dalvik.vm.image-dex2oat-filter "verify-at-runtime" - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libart.so - $(ADB) shell setprop dalvik.vm.usejit true - $(ADB) shell start - -.PHONY: use-art-interpret-only -use-art-interpret-only: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/* - $(ADB) shell setprop dalvik.vm.dex2oat-filter "interpret-only" - $(ADB) shell setprop dalvik.vm.image-dex2oat-filter "interpret-only" - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libart.so - $(ADB) shell setprop dalvik.vm.usejit false - $(ADB) shell start - -.PHONY: use-artd-interpret-only -use-artd-interpret-only: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/* - $(ADB) shell setprop dalvik.vm.dex2oat-filter "interpret-only" - $(ADB) shell setprop dalvik.vm.image-dex2oat-filter "interpret-only" - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libartd.so - $(ADB) shell setprop dalvik.vm.usejit false - $(ADB) shell start - -.PHONY: use-art-verify-none -use-art-verify-none: - $(ADB) root - $(ADB) wait-for-device shell stop - $(ADB) shell rm -rf $(ART_TARGET_DALVIK_CACHE_DIR)/* - $(ADB) shell setprop dalvik.vm.dex2oat-filter "verify-none" - $(ADB) shell setprop dalvik.vm.image-dex2oat-filter "verify-none" - $(ADB) shell setprop persist.sys.dalvik.vm.lib.2 libart.so - $(ADB) shell setprop dalvik.vm.usejit false - $(ADB) shell start - -######################################################################## # Clear locally used variables. TEST_ART_TARGET_SYNC_DEPS := diff --git a/TEST_MAPPING b/TEST_MAPPING index 3d144250fd..1b48489d6e 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -1,5 +1,1433 @@ // Generated by `regen-test-files`. Do not edit manually. { + "art-mainline-presubmit": [ + { + "name": "art-run-test-001-HelloWorld" + }, + { + "name": "art-run-test-001-Main" + }, + { + "name": "art-run-test-002-sleep" + }, + { + "name": "art-run-test-004-InterfaceTest" + }, + { + "name": "art-run-test-004-checker-UnsafeTest18" + }, + { + "name": "art-run-test-006-args" + }, + { + "name": "art-run-test-007-count10" + }, + { + "name": "art-run-test-009-instanceof" + }, + { + "name": "art-run-test-010-instance" + }, + { + "name": "art-run-test-011-array-copy" + }, + { + "name": "art-run-test-012-math" + }, + { + "name": "art-run-test-013-math2" + }, + { + "name": "art-run-test-014-math3" + }, + { + "name": "art-run-test-015-checker-switch" + }, + { + "name": "art-run-test-016-intern" + }, + { + "name": "art-run-test-017-float" + }, + { + "name": "art-run-test-018-stack-overflow" + }, + { + "name": "art-run-test-019-wrong-array-type" + }, + { + "name": "art-run-test-020-string" + }, + { + "name": "art-run-test-021-string2" + }, + { + "name": "art-run-test-022-interface" + }, + { + "name": "art-run-test-024-illegal-access" + }, + { + "name": "art-run-test-025-access-controller" + }, + { + "name": "art-run-test-026-access" + }, + { + "name": "art-run-test-027-arithmetic" + }, + { + "name": "art-run-test-028-array-write" + }, + { + "name": "art-run-test-029-assert" + }, + { + "name": "art-run-test-032-concrete-sub" + }, + { + "name": "art-run-test-033-class-init-deadlock" + }, + { + "name": "art-run-test-035-enum" + }, + { + "name": "art-run-test-036-finalizer" + }, + { + "name": "art-run-test-037-inherit" + }, + { + "name": "art-run-test-039-join-main" + }, + { + "name": "art-run-test-040-miranda" + }, + { + "name": "art-run-test-041-narrowing" + }, + { + "name": "art-run-test-042-new-instance" + }, + { + "name": "art-run-test-043-privates" + }, + { + "name": "art-run-test-045-reflect-array" + }, + { + "name": "art-run-test-046-reflect" + }, + { + "name": "art-run-test-047-returns" + }, + { + "name": "art-run-test-048-reflect-v8" + }, + { + "name": "art-run-test-049-show-object" + }, + { + "name": "art-run-test-050-sync-test" + }, + { + "name": "art-run-test-052-verifier-fun" + }, + { + "name": "art-run-test-053-wait-some" + }, + { + "name": "art-run-test-055-enum-performance" + }, + { + "name": "art-run-test-058-enum-order" + }, + { + "name": "art-run-test-059-finalizer-throw" + }, + { + "name": "art-run-test-061-out-of-memory" + }, + { + "name": "art-run-test-062-character-encodings" + }, + { + "name": "art-run-test-063-process-manager" + }, + { + "name": "art-run-test-067-preemptive-unpark" + }, + { + "name": "art-run-test-069-field-type" + }, + { + "name": "art-run-test-070-nio-buffer" + }, + { + "name": "art-run-test-072-precise-gc" + }, + { + "name": "art-run-test-072-reachability-fence" + }, + { + "name": "art-run-test-073-mismatched-field" + }, + { + "name": "art-run-test-074-gc-thrash" + }, + { + "name": "art-run-test-075-verification-error" + }, + { + "name": "art-run-test-076-boolean-put" + }, + { + "name": "art-run-test-077-method-override" + }, + { + "name": "art-run-test-078-polymorphic-virtual" + }, + { + "name": "art-run-test-079-phantom" + }, + { + "name": "art-run-test-080-oom-fragmentation" + }, + { + "name": "art-run-test-080-oom-throw" + }, + { + "name": "art-run-test-080-oom-throw-with-finalizer" + }, + { + "name": "art-run-test-081-hot-exceptions" + }, + { + "name": "art-run-test-082-inline-execute" + }, + { + "name": "art-run-test-083-compiler-regressions" + }, + { + "name": "art-run-test-084-class-init" + }, + { + "name": "art-run-test-090-loop-formation" + }, + { + "name": "art-run-test-092-locale" + }, + { + "name": "art-run-test-093-serialization" + }, + { + "name": "art-run-test-094-pattern" + }, + { + "name": "art-run-test-095-switch-MAX_INT" + }, + { + "name": "art-run-test-096-array-copy-concurrent-gc" + }, + { + "name": "art-run-test-099-vmdebug" + }, + { + "name": "art-run-test-100-reflect2" + }, + { + "name": "art-run-test-1000-non-moving-space-stress" + }, + { + "name": "art-run-test-1004-checker-volatile-ref-load" + }, + { + "name": "art-run-test-101-fibonacci" + }, + { + "name": "art-run-test-102-concurrent-gc" + }, + { + "name": "art-run-test-103-string-append" + }, + { + "name": "art-run-test-104-growth-limit" + }, + { + "name": "art-run-test-105-invoke" + }, + { + "name": "art-run-test-106-exceptions2" + }, + { + "name": "art-run-test-107-int-math2" + }, + { + "name": "art-run-test-108-check-cast" + }, + { + "name": "art-run-test-109-suspend-check" + }, + { + "name": "art-run-test-110-field-access" + }, + { + "name": "art-run-test-112-double-math" + }, + { + "name": "art-run-test-114-ParallelGC" + }, + { + "name": "art-run-test-120-hashcode" + }, + { + "name": "art-run-test-121-simple-suspend-check" + }, + { + "name": "art-run-test-122-npe" + }, + { + "name": "art-run-test-123-compiler-regressions-mt" + }, + { + "name": "art-run-test-123-inline-execute2" + }, + { + "name": "art-run-test-125-gc-and-classloading" + }, + { + "name": "art-run-test-128-reg-spill-on-implicit-nullcheck" + }, + { + "name": "art-run-test-129-ThreadGetId" + }, + { + "name": "art-run-test-132-daemon-locks-shutdown" + }, + { + "name": "art-run-test-133-static-invoke-super" + }, + { + "name": "art-run-test-1338-gc-no-los" + }, + { + "name": "art-run-test-140-dce-regression" + }, + { + "name": "art-run-test-140-field-packing" + }, + { + "name": "art-run-test-143-string-value" + }, + { + "name": "art-run-test-145-alloc-tracking-stress" + }, + { + "name": "art-run-test-152-dead-large-object" + }, + { + "name": "art-run-test-153-reference-stress" + }, + { + "name": "art-run-test-156-register-dex-file-multi-loader" + }, + { + "name": "art-run-test-159-app-image-fields" + }, + { + "name": "art-run-test-160-read-barrier-stress" + }, + { + "name": "art-run-test-163-app-image-methods" + }, + { + "name": "art-run-test-165-lock-owner-proxy" + }, + { + "name": "art-run-test-168-vmstack-annotated" + }, + { + "name": "art-run-test-170-interface-init" + }, + { + "name": "art-run-test-174-escaping-instance-of-bad-class" + }, + { + "name": "art-run-test-175-alloc-big-bignums" + }, + { + "name": "art-run-test-176-app-image-string" + }, + { + "name": "art-run-test-182-method-linking" + }, + { + "name": "art-run-test-1960-checker-bounds-codegen" + }, + { + "name": "art-run-test-1961-checker-loop-vectorizer" + }, + { + "name": "art-run-test-201-built-in-except-detail-messages" + }, + { + "name": "art-run-test-2019-constantcalculationsinking" + }, + { + "name": "art-run-test-202-thread-oome" + }, + { + "name": "art-run-test-2020-InvokeVirtual-Inlining" + }, + { + "name": "art-run-test-2021-InvokeStatic-Inlining" + }, + { + "name": "art-run-test-2022-Invariantloops" + }, + { + "name": "art-run-test-2023-InvariantLoops_typecast" + }, + { + "name": "art-run-test-2024-InvariantNegativeLoop" + }, + { + "name": "art-run-test-2025-ChangedArrayValue" + }, + { + "name": "art-run-test-2026-DifferentMemoryLSCouples" + }, + { + "name": "art-run-test-2027-TwiceTheSameMemoryCouple" + }, + { + "name": "art-run-test-2028-MultiBackward" + }, + { + "name": "art-run-test-2029-contended-monitors" + }, + { + "name": "art-run-test-2030-long-running-child" + }, + { + "name": "art-run-test-2042-checker-dce-always-throw" + }, + { + "name": "art-run-test-2042-reference-processing" + }, + { + "name": "art-run-test-2043-reference-pauses" + }, + { + "name": "art-run-test-2044-get-stack-traces" + }, + { + "name": "art-run-test-2046-checker-comparison" + }, + { + "name": "art-run-test-2047-checker-const-string-length" + }, + { + "name": "art-run-test-2231-checker-heap-poisoning" + }, + { + "name": "art-run-test-2233-checker-remove-loop-suspend-check" + }, + { + "name": "art-run-test-2234-checker-remove-entry-suspendcheck" + }, + { + "name": "art-run-test-2236-JdkUnsafeGetLong-regression" + }, + { + "name": "art-run-test-2241-checker-inline-try-catch" + }, + { + "name": "art-run-test-2242-checker-lse-acquire-release-operations" + }, + { + "name": "art-run-test-2243-checker-not-inline-into-throw" + }, + { + "name": "art-run-test-2244-checker-remove-try-boundary" + }, + { + "name": "art-run-test-2249-checker-return-try-boundary-exit-in-loop" + }, + { + "name": "art-run-test-2250-inline-throw-into-try" + }, + { + "name": "art-run-test-2252-rem-optimization-dividend-divisor" + }, + { + "name": "art-run-test-2253-checker-devirtualize-always-throws" + }, + { + "name": "art-run-test-2254-checker-not-var-analyzed-pathological" + }, + { + "name": "art-run-test-2254-class-value-before-and-after-u" + }, + { + "name": "art-run-test-2255-checker-branch-redirection" + }, + { + "name": "art-run-test-2256-checker-vector-replacement" + }, + { + "name": "art-run-test-2257-checker-constant-folding-before-codegen" + }, + { + "name": "art-run-test-2258-checker-valid-rti" + }, + { + "name": "art-run-test-2259-checker-code-sinking-infinite-try-catch" + }, + { + "name": "art-run-test-2260-checker-inline-unimplemented-intrinsics" + }, + { + "name": "art-run-test-2262-checker-return-sinking" + }, + { + "name": "art-run-test-2265-checker-select-binary-unary" + }, + { + "name": "art-run-test-2266-checker-remove-empty-ifs" + }, + { + "name": "art-run-test-2268-checker-remove-dead-phis" + }, + { + "name": "art-run-test-2269-checker-constant-folding-instrinsics" + }, + { + "name": "art-run-test-300-package-override" + }, + { + "name": "art-run-test-301-abstract-protected" + }, + { + "name": "art-run-test-302-float-conversion" + }, + { + "name": "art-run-test-304-method-tracing" + }, + { + "name": "art-run-test-401-optimizing-compiler" + }, + { + "name": "art-run-test-402-optimizing-control-flow" + }, + { + "name": "art-run-test-403-optimizing-long" + }, + { + "name": "art-run-test-404-optimizing-allocator" + }, + { + "name": "art-run-test-405-optimizing-long-allocator" + }, + { + "name": "art-run-test-406-fields" + }, + { + "name": "art-run-test-407-arrays" + }, + { + "name": "art-run-test-408-move-bug" + }, + { + "name": "art-run-test-409-materialized-condition" + }, + { + "name": "art-run-test-410-floats" + }, + { + "name": "art-run-test-411-checker-hdiv-hrem-const" + }, + { + "name": "art-run-test-411-checker-hdiv-hrem-pow2" + }, + { + "name": "art-run-test-411-checker-instruct-simplifier-hrem" + }, + { + "name": "art-run-test-411-optimizing-arith" + }, + { + "name": "art-run-test-413-regalloc-regression" + }, + { + "name": "art-run-test-414-static-fields" + }, + { + "name": "art-run-test-418-const-string" + }, + { + "name": "art-run-test-419-long-parameter" + }, + { + "name": "art-run-test-420-const-class" + }, + { + "name": "art-run-test-421-exceptions" + }, + { + "name": "art-run-test-421-large-frame" + }, + { + "name": "art-run-test-422-instanceof" + }, + { + "name": "art-run-test-422-type-conversion" + }, + { + "name": "art-run-test-423-invoke-interface" + }, + { + "name": "art-run-test-424-checkcast" + }, + { + "name": "art-run-test-426-monitor" + }, + { + "name": "art-run-test-427-bitwise" + }, + { + "name": "art-run-test-427-bounds" + }, + { + "name": "art-run-test-429-ssa-builder" + }, + { + "name": "art-run-test-430-live-register-slow-path" + }, + { + "name": "art-run-test-433-gvn" + }, + { + "name": "art-run-test-434-shifter-operand" + }, + { + "name": "art-run-test-435-try-finally-without-catch" + }, + { + "name": "art-run-test-436-rem-float" + }, + { + "name": "art-run-test-436-shift-constant" + }, + { + "name": "art-run-test-437-inline" + }, + { + "name": "art-run-test-438-volatile" + }, + { + "name": "art-run-test-439-npe" + }, + { + "name": "art-run-test-439-swap-double" + }, + { + "name": "art-run-test-440-stmp" + }, + { + "name": "art-run-test-441-checker-inliner" + }, + { + "name": "art-run-test-443-not-bool-inline" + }, + { + "name": "art-run-test-444-checker-nce" + }, + { + "name": "art-run-test-445-checker-licm" + }, + { + "name": "art-run-test-446-checker-inliner2" + }, + { + "name": "art-run-test-447-checker-inliner3" + }, + { + "name": "art-run-test-449-checker-bce-rem" + }, + { + "name": "art-run-test-450-checker-types" + }, + { + "name": "art-run-test-451-regression-add-float" + }, + { + "name": "art-run-test-451-spill-splot" + }, + { + "name": "art-run-test-455-checker-gvn" + }, + { + "name": "art-run-test-456-baseline-array-set" + }, + { + "name": "art-run-test-458-long-to-fpu" + }, + { + "name": "art-run-test-464-checker-inline-sharpen-calls" + }, + { + "name": "art-run-test-465-checker-clinit-gvn" + }, + { + "name": "art-run-test-469-condition-materialization" + }, + { + "name": "art-run-test-470-huge-method" + }, + { + "name": "art-run-test-471-deopt-environment" + }, + { + "name": "art-run-test-472-type-propagation" + }, + { + "name": "art-run-test-473-checker-inliner-constants" + }, + { + "name": "art-run-test-473-remove-dead-block" + }, + { + "name": "art-run-test-474-checker-boolean-input" + }, + { + "name": "art-run-test-474-fp-sub-neg" + }, + { + "name": "art-run-test-475-simplify-mul-zero" + }, + { + "name": "art-run-test-476-checker-ctor-fence-redun-elim" + }, + { + "name": "art-run-test-476-checker-ctor-memory-barrier" + }, + { + "name": "art-run-test-476-clinit-inline-static-invoke" + }, + { + "name": "art-run-test-477-checker-bound-type" + }, + { + "name": "art-run-test-477-long-2-float-convers-precision" + }, + { + "name": "art-run-test-478-checker-clinit-check-pruning" + }, + { + "name": "art-run-test-478-checker-inline-noreturn" + }, + { + "name": "art-run-test-478-checker-inliner-nested-loop" + }, + { + "name": "art-run-test-479-regression-implicit-null-check" + }, + { + "name": "art-run-test-480-checker-dead-blocks" + }, + { + "name": "art-run-test-481-regression-phi-cond" + }, + { + "name": "art-run-test-482-checker-loop-back-edge-use" + }, + { + "name": "art-run-test-483-dce-block" + }, + { + "name": "art-run-test-485-checker-dce-switch" + }, + { + "name": "art-run-test-486-checker-must-do-null-check" + }, + { + "name": "art-run-test-487-checker-inline-calls" + }, + { + "name": "art-run-test-488-checker-inline-recursive-calls" + }, + { + "name": "art-run-test-489-current-method-regression" + }, + { + "name": "art-run-test-490-checker-inline" + }, + { + "name": "art-run-test-491-current-method" + }, + { + "name": "art-run-test-492-checker-inline-invoke-interface" + }, + { + "name": "art-run-test-493-checker-inline-invoke-interface" + }, + { + "name": "art-run-test-494-checker-instanceof-tests" + }, + { + "name": "art-run-test-495-checker-checkcast-tests" + }, + { + "name": "art-run-test-496-checker-inlining-class-loader" + }, + { + "name": "art-run-test-499-bce-phi-array-length" + }, + { + "name": "art-run-test-500-instanceof" + }, + { + "name": "art-run-test-505-simplifier-type-propagation" + }, + { + "name": "art-run-test-507-boolean-test" + }, + { + "name": "art-run-test-507-referrer" + }, + { + "name": "art-run-test-508-checker-disassembly" + }, + { + "name": "art-run-test-508-referrer-method" + }, + { + "name": "art-run-test-513-array-deopt" + }, + { + "name": "art-run-test-514-shifts" + }, + { + "name": "art-run-test-519-bound-load-class" + }, + { + "name": "art-run-test-521-checker-array-set-null" + }, + { + "name": "art-run-test-521-regression-integer-field-set" + }, + { + "name": "art-run-test-524-boolean-simplifier-regression" + }, + { + "name": "art-run-test-525-checker-arrays-fields1" + }, + { + "name": "art-run-test-525-checker-arrays-fields2" + }, + { + "name": "art-run-test-526-checker-caller-callee-regs" + }, + { + "name": "art-run-test-526-long-regalloc" + }, + { + "name": "art-run-test-527-checker-array-access-simd" + }, + { + "name": "art-run-test-527-checker-array-access-split" + }, + { + "name": "art-run-test-528-long-hint" + }, + { + "name": "art-run-test-529-long-split" + }, + { + "name": "art-run-test-530-checker-loops-try-catch" + }, + { + "name": "art-run-test-530-checker-loops1" + }, + { + "name": "art-run-test-530-checker-loops2" + }, + { + "name": "art-run-test-530-checker-loops3" + }, + { + "name": "art-run-test-530-checker-loops4" + }, + { + "name": "art-run-test-530-checker-loops5" + }, + { + "name": "art-run-test-530-checker-lse" + }, + { + "name": "art-run-test-530-checker-lse-ctor-fences" + }, + { + "name": "art-run-test-530-checker-lse-simd" + }, + { + "name": "art-run-test-530-checker-lse-try-catch" + }, + { + "name": "art-run-test-530-instanceof-checkcast" + }, + { + "name": "art-run-test-532-checker-nonnull-arrayset" + }, + { + "name": "art-run-test-534-checker-bce-deoptimization" + }, + { + "name": "art-run-test-535-deopt-and-inlining" + }, + { + "name": "art-run-test-536-checker-intrinsic-optimization" + }, + { + "name": "art-run-test-536-checker-needs-access-check" + }, + { + "name": "art-run-test-537-checker-arraycopy" + }, + { + "name": "art-run-test-537-checker-inline-and-unverified" + }, + { + "name": "art-run-test-537-checker-jump-over-jump" + }, + { + "name": "art-run-test-538-checker-embed-constants" + }, + { + "name": "art-run-test-540-checker-rtp-bug" + }, + { + "name": "art-run-test-542-bitfield-rotates" + }, + { + "name": "art-run-test-542-inline-trycatch" + }, + { + "name": "art-run-test-542-unresolved-access-check" + }, + { + "name": "art-run-test-545-tracing-and-jit" + }, + { + "name": "art-run-test-548-checker-inlining-and-dce" + }, + { + "name": "art-run-test-549-checker-types-merge" + }, + { + "name": "art-run-test-550-checker-multiply-accumulate" + }, + { + "name": "art-run-test-550-new-instance-clinit" + }, + { + "name": "art-run-test-551-checker-clinit" + }, + { + "name": "art-run-test-551-checker-shifter-operand" + }, + { + "name": "art-run-test-551-implicit-null-checks" + }, + { + "name": "art-run-test-552-checker-x86-avx2-bit-manipulation" + }, + { + "name": "art-run-test-554-checker-rtp-checkcast" + }, + { + "name": "art-run-test-557-checker-instruct-simplifier-ror" + }, + { + "name": "art-run-test-558-switch" + }, + { + "name": "art-run-test-559-bce-ssa" + }, + { + "name": "art-run-test-559-checker-rtp-ifnotnull" + }, + { + "name": "art-run-test-560-packed-switch" + }, + { + "name": "art-run-test-561-divrem" + }, + { + "name": "art-run-test-561-shared-slowpaths" + }, + { + "name": "art-run-test-562-bce-preheader" + }, + { + "name": "art-run-test-562-checker-no-intermediate" + }, + { + "name": "art-run-test-563-checker-invoke-super" + }, + { + "name": "art-run-test-564-checker-bitcount" + }, + { + "name": "art-run-test-564-checker-inline-loop" + }, + { + "name": "art-run-test-564-checker-negbitwise" + }, + { + "name": "art-run-test-565-checker-condition-liveness" + }, + { + "name": "art-run-test-566-checker-codegen-select" + }, + { + "name": "art-run-test-567-checker-builder-intrinsics" + }, + { + "name": "art-run-test-568-checker-onebit" + }, + { + "name": "art-run-test-570-checker-select" + }, + { + "name": "art-run-test-572-checker-array-get-regression" + }, + { + "name": "art-run-test-573-checker-checkcast-regression" + }, + { + "name": "art-run-test-576-polymorphic-inlining" + }, + { + "name": "art-run-test-577-checker-fp2int" + }, + { + "name": "art-run-test-578-bce-visit" + }, + { + "name": "art-run-test-578-polymorphic-inlining" + }, + { + "name": "art-run-test-579-inline-infinite" + }, + { + "name": "art-run-test-580-checker-fp16" + }, + { + "name": "art-run-test-580-checker-round" + }, + { + "name": "art-run-test-580-checker-string-fact-intrinsics" + }, + { + "name": "art-run-test-580-crc32" + }, + { + "name": "art-run-test-581-checker-rtp" + }, + { + "name": "art-run-test-582-checker-bce-length" + }, + { + "name": "art-run-test-583-checker-zero" + }, + { + "name": "art-run-test-584-checker-div-bool" + }, + { + "name": "art-run-test-589-super-imt" + }, + { + "name": "art-run-test-590-checker-arr-set-null-regression" + }, + { + "name": "art-run-test-591-checker-regression-dead-loop" + }, + { + "name": "art-run-test-593-checker-long-2-float-regression" + }, + { + "name": "art-run-test-594-checker-array-alias" + }, + { + "name": "art-run-test-594-load-string-regression" + }, + { + "name": "art-run-test-603-checker-instanceof" + }, + { + "name": "art-run-test-605-new-string-from-bytes" + }, + { + "name": "art-run-test-607-daemon-stress" + }, + { + "name": "art-run-test-609-checker-inline-interface" + }, + { + "name": "art-run-test-609-checker-x86-bounds-check" + }, + { + "name": "art-run-test-610-arraycopy" + }, + { + "name": "art-run-test-611-checker-simplify-if" + }, + { + "name": "art-run-test-614-checker-dump-constant-location" + }, + { + "name": "art-run-test-615-checker-arm64-store-zero" + }, + { + "name": "art-run-test-617-clinit-oome" + }, + { + "name": "art-run-test-618-checker-induction" + }, + { + "name": "art-run-test-619-checker-current-method" + }, + { + "name": "art-run-test-620-checker-bce-intrinsics" + }, + { + "name": "art-run-test-622-checker-bce-regressions" + }, + { + "name": "art-run-test-625-checker-licm-regressions" + }, + { + "name": "art-run-test-627-checker-unroll" + }, + { + "name": "art-run-test-628-vdex" + }, + { + "name": "art-run-test-631-checker-get-class" + }, + { + "name": "art-run-test-632-checker-char-at-bounds" + }, + { + "name": "art-run-test-635-checker-arm64-volatile-load-cc" + }, + { + "name": "art-run-test-636-arm64-veneer-pool" + }, + { + "name": "art-run-test-637-checker-throw-inline" + }, + { + "name": "art-run-test-639-checker-code-sinking" + }, + { + "name": "art-run-test-640-checker-boolean-simd" + }, + { + "name": "art-run-test-640-checker-integer-valueof" + }, + { + "name": "art-run-test-640-checker-simd" + }, + { + "name": "art-run-test-641-checker-arraycopy" + }, + { + "name": "art-run-test-641-iterations" + }, + { + "name": "art-run-test-643-checker-bogus-ic" + }, + { + "name": "art-run-test-645-checker-abs-simd" + }, + { + "name": "art-run-test-646-checker-arraycopy-large-cst-pos" + }, + { + "name": "art-run-test-646-checker-long-const-to-int" + }, + { + "name": "art-run-test-646-checker-simd-hadd" + }, + { + "name": "art-run-test-650-checker-inline-access-thunks" + }, + { + "name": "art-run-test-654-checker-periodic" + }, + { + "name": "art-run-test-655-checker-simd-arm-opt" + }, + { + "name": "art-run-test-656-checker-simd-opt" + }, + { + "name": "art-run-test-657-branches" + }, + { + "name": "art-run-test-658-fp-read-barrier" + }, + { + "name": "art-run-test-659-unpadded-array" + }, + { + "name": "art-run-test-660-checker-sad" + }, + { + "name": "art-run-test-660-checker-simd-sad" + }, + { + "name": "art-run-test-661-checker-simd-cf-loops" + }, + { + "name": "art-run-test-661-checker-simd-reduc" + }, + { + "name": "art-run-test-662-regression-alias" + }, + { + "name": "art-run-test-663-checker-select-generator" + }, + { + "name": "art-run-test-665-checker-simd-zero" + }, + { + "name": "art-run-test-666-dex-cache-itf" + }, + { + "name": "art-run-test-667-checker-simd-alignment" + }, + { + "name": "art-run-test-667-out-of-bounds" + }, + { + "name": "art-run-test-669-checker-break" + }, + { + "name": "art-run-test-671-npe-field-opts" + }, + { + "name": "art-run-test-672-checker-throw-method" + }, + { + "name": "art-run-test-673-checker-throw-vmethod" + }, + { + "name": "art-run-test-676-proxy-jit-at-first-use" + }, + { + "name": "art-run-test-677-fsi2" + }, + { + "name": "art-run-test-678-quickening" + }, + { + "name": "art-run-test-684-checker-simd-dotprod" + }, + { + "name": "art-run-test-684-select-condition" + }, + { + "name": "art-run-test-689-multi-catch" + }, + { + "name": "art-run-test-694-clinit-jit" + }, + { + "name": "art-run-test-695-simplify-throws" + }, + { + "name": "art-run-test-696-loop" + }, + { + "name": "art-run-test-697-checker-string-append" + }, + { + "name": "art-run-test-698-selects" + }, + { + "name": "art-run-test-700-LoadArgRegs" + }, + { + "name": "art-run-test-703-floating-point-div" + }, + { + "name": "art-run-test-704-multiply-accumulate" + }, + { + "name": "art-run-test-705-register-conflict" + }, + { + "name": "art-run-test-711-checker-type-conversion" + }, + { + "name": "art-run-test-713-varhandle-invokers" + }, + { + "name": "art-run-test-718-zipfile-finalizer" + }, + { + "name": "art-run-test-719-varhandle-concurrency" + }, + { + "name": "art-run-test-721-osr" + }, + { + "name": "art-run-test-726-array-store" + }, + { + "name": "art-run-test-730-checker-inlining-super" + }, + { + "name": "art-run-test-731-bounds-check-slow-path" + }, + { + "name": "art-run-test-805-TooDeepClassInstanceOf" + }, + { + "name": "art-run-test-806-TooWideClassInstanceOf" + }, + { + "name": "art-run-test-812-recursive-default" + }, + { + "name": "art-run-test-814-large-field-offsets" + }, + { + "name": "art-run-test-815-invokeinterface-default" + }, + { + "name": "art-run-test-818-clinit-nterp" + }, + { + "name": "art-run-test-821-madvise-willneed" + }, + { + "name": "art-run-test-828-partial-lse" + }, + { + "name": "art-run-test-834-lse" + }, + { + "name": "art-run-test-835-b216762268" + }, + { + "name": "art-run-test-838-override" + }, + { + "name": "art-run-test-839-clinit-throw" + }, + { + "name": "art-run-test-841-defaults" + }, + { + "name": "art-run-test-843-default-interface" + }, + { + "name": "art-run-test-851-null-instanceof" + }, + { + "name": "art-run-test-853-checker-inlining" + }, + { + "name": "art-run-test-963-default-range-smali" + }, + { + "name": "art-run-test-965-default-verify" + }, + { + "name": "art-run-test-967-default-ame" + }, + { + "name": "art_libnativebridge_cts_tests" + }, + { + "name": "art_standalone_artd_tests" + }, + { + "name": "art_standalone_cmdline_tests" + }, + { + "name": "art_standalone_compiler_tests", + "options": [ + { + "exclude-filter": "JniCompilerTest*" + } + ] + }, + { + "name": "art_standalone_dex2oat_tests" + }, + { + "name": "art_standalone_dexdump_tests" + }, + { + "name": "art_standalone_dexlist_tests" + }, + { + "name": "art_standalone_dexoptanalyzer_tests" + }, + { + "name": "art_standalone_libartbase_tests" + }, + { + "name": "art_standalone_libartpalette_tests", + "options": [ + { + "exclude-filter": "PaletteClientJniTest*" + } + ] + }, + { + "name": "art_standalone_libartservice_tests" + }, + { + "name": "art_standalone_libarttools_tests" + }, + { + "name": "art_standalone_libdexfile_support_tests" + }, + { + "name": "art_standalone_libdexfile_tests" + }, + { + "name": "art_standalone_libprofile_tests" + }, + { + "name": "art_standalone_oatdump_tests" + }, + { + "name": "art_standalone_odrefresh_tests" + }, + { + "name": "art_standalone_profman_tests" + }, + { + "name": "art_standalone_runtime_tests" + }, + { + "name": "art_standalone_sigchain_tests" + }, + { + "name": "libnativeloader_e2e_tests" + }, + { + "name": "libnativeloader_test" + } + ], "mainline-presubmit": [ { "name": "art-run-test-001-HelloWorld[com.google.android.art.apex]" @@ -1422,6 +2850,9 @@ "name": "art_standalone_sigchain_tests[com.google.android.art.apex]" }, { + "name": "libnativebridge-tests[com.google.android.art.apex]" + }, + { "name": "libnativeloader_e2e_tests[com.google.android.art.apex]" }, { @@ -2858,6 +4289,9 @@ "name": "art_standalone_sigchain_tests" }, { + "name": "libnativebridge-tests" + }, + { "name": "libnativeloader_e2e_tests" }, { @@ -4288,6 +5722,9 @@ "name": "art_standalone_sigchain_tests" }, { + "name": "libnativebridge-tests" + }, + { "name": "libnativeloader_e2e_tests" }, { @@ -4305,9 +5742,6 @@ }, { "name": "art-run-test-2273-checker-unreachable-intrinsics" - }, - { - "name": "libnativebridge-tests" } ] } diff --git a/adbconnection/adbconnection.cc b/adbconnection/adbconnection.cc index 55eac932d3..2ef7a842fa 100644 --- a/adbconnection/adbconnection.cc +++ b/adbconnection/adbconnection.cc @@ -16,6 +16,7 @@ #include "adbconnection.h" +#include <dlfcn.h> #include <jni.h> #include <sys/eventfd.h> #include <sys/ioctl.h> @@ -74,8 +75,7 @@ using dt_fd_forward::kSkipHandshakeMessage; using android::base::StringPrintf; static constexpr const char kJdwpHandshake[14] = { - 'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e' -}; + 'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'}; static constexpr int kEventfdLocked = 0; static constexpr int kEventfdUnlocked = 1; @@ -92,8 +92,88 @@ static constexpr uint8_t kDdmChunkCommand = 1; static std::optional<AdbConnectionState> gState; static std::optional<pthread_t> gPthread; -static bool IsDebuggingPossible() { - return art::Dbg::IsJdwpAllowed(); +// ADB apex method v2 +using AdbApexProcessName = void (*)(const char*); +AdbApexProcessName apex_adbconnection_client_set_current_process_name = nullptr; +using AdbApexPackageName = void (*)(const char*); +AdbApexPackageName apex_adbconnection_client_add_application = nullptr; +AdbApexPackageName apex_adbconnection_client_remove_application = nullptr; +using AdbApexWaitingForDebugger = void (*)(bool); +AdbApexWaitingForDebugger apex_adbconnection_client_set_waiting_for_debugger = nullptr; +using AdbApexSendUpdate = void (*)(AdbConnectionClientContext*); +AdbApexSendUpdate apex_adbconnection_client_send_update = nullptr; +using AdbApexHasPendingUpdate = bool (*)(); +AdbApexHasPendingUpdate apex_adbconnection_client_has_pending_update = nullptr; +using AdbApexSetUserId = void (*)(int); +AdbApexSetUserId apex_adbconnection_client_set_user_id = nullptr; + +void apex_adbconnection_client_set_current_process_name_noop(const char*) {} +void apex_adbconnection_client_add_application_noop(const char*) {} +void apex_adbconnection_client_remove_application_noop(const char*) {} +void apex_adbconnection_client_set_waiting_for_debugger_noop(bool) {} +void apex_adbconnection_client_send_update_noop(AdbConnectionClientContext*) {} +bool apex_adbconnection_client_has_pending_update_noop() { return false; } +void apex_adbconnection_client_set_user_id_noop(int) {} + +static void RetrieveApexPointers() { + apex_adbconnection_client_set_current_process_name = + (AdbApexProcessName)dlsym(RTLD_DEFAULT, "adbconnection_client_set_current_process_name"); + if (!apex_adbconnection_client_set_current_process_name) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_set_current_process_name"; + apex_adbconnection_client_set_current_process_name = + apex_adbconnection_client_set_current_process_name_noop; + } + + apex_adbconnection_client_add_application = + (AdbApexPackageName)dlsym(RTLD_DEFAULT, "adbconnection_client_add_application"); + if (!apex_adbconnection_client_add_application) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_add_application"; + apex_adbconnection_client_add_application = apex_adbconnection_client_add_application_noop; + } + + apex_adbconnection_client_remove_application = + (AdbApexPackageName)dlsym(RTLD_DEFAULT, "adbconnection_client_remove_application"); + if (!apex_adbconnection_client_remove_application) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_remove_application"; + apex_adbconnection_client_remove_application = + apex_adbconnection_client_remove_application_noop; + } + + apex_adbconnection_client_set_waiting_for_debugger = (AdbApexWaitingForDebugger)dlsym( + RTLD_DEFAULT, "adbconnection_client_set_waiting_for_debugger"); + if (!apex_adbconnection_client_set_waiting_for_debugger) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_set_waiting_for_debugger"; + apex_adbconnection_client_set_waiting_for_debugger = + apex_adbconnection_client_set_waiting_for_debugger_noop; + } + + apex_adbconnection_client_send_update = + (AdbApexSendUpdate)dlsym(RTLD_DEFAULT, "adbconnection_client_send_update"); + if (!apex_adbconnection_client_send_update) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_send_update"; + apex_adbconnection_client_send_update = apex_adbconnection_client_send_update_noop; + } + + apex_adbconnection_client_has_pending_update = + (AdbApexHasPendingUpdate)dlsym(RTLD_DEFAULT, "adbconnection_client_has_pending_update"); + if (!apex_adbconnection_client_has_pending_update) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_has_pending_update"; + apex_adbconnection_client_has_pending_update = + apex_adbconnection_client_has_pending_update_noop; + } + + apex_adbconnection_client_set_user_id = + (AdbApexSetUserId)dlsym(RTLD_DEFAULT, "adbconnection_client_set_user_id"); + if (!apex_adbconnection_client_set_user_id) { + VLOG(jdwp) << "Unable to dlsym adbconnection_client_set_user_id"; + apex_adbconnection_client_set_user_id = apex_adbconnection_client_set_user_id_noop; + } +} + +static bool IsDebuggingPossible() { return art::Dbg::IsJdwpAllowed(); } + +static bool IsDebuggableOrProfilable() { + return IsDebuggingPossible() || art::Runtime::Current()->IsProfileableFromShell(); } // Begin running the debugger. @@ -101,7 +181,7 @@ void AdbConnectionDebuggerController::StartDebugger() { // The debugger thread is started for a debuggable or profileable-from-shell process. // The pid will be send to adbd for adb's "track-jdwp" and "track-app" services. // The thread will also set up the jdwp tunnel if the process is debuggable. - if (IsDebuggingPossible() || art::Runtime::Current()->IsProfileableFromShell()) { + if (IsDebuggableOrProfilable()) { connection_->StartDebuggerThreads(); } else { LOG(ERROR) << "Not starting debugger since process cannot load the jdwp agent."; @@ -135,15 +215,31 @@ void AdbConnectionDdmCallback::DdmPublishChunk(uint32_t type, connection_->PublishDdmData(type, data); } +void AdbConnectionAppInfoCallback::SetCurrentProcessName(const std::string& process_name) { + connection_->SetCurrentProcessName(process_name); +} + +void AdbConnectionAppInfoCallback::AddApplication(const std::string& package_name) { + connection_->AddApplication(package_name); +} + +void AdbConnectionAppInfoCallback::RemoveApplication(const std::string& package_name) { + connection_->RemoveApplication(package_name); +} + +void AdbConnectionAppInfoCallback::SetWaitingForDebugger(bool waiting) { + connection_->SetWaitingForDebugger(waiting); +} + +void AdbConnectionAppInfoCallback::SetUserId(int user_id) { connection_->SetUserId(user_id); } + class ScopedEventFdLock { public: explicit ScopedEventFdLock(int fd) : fd_(fd), data_(0) { TEMP_FAILURE_RETRY(read(fd_, &data_, sizeof(data_))); } - ~ScopedEventFdLock() { - TEMP_FAILURE_RETRY(write(fd_, &data_, sizeof(data_))); - } + ~ScopedEventFdLock() { TEMP_FAILURE_RETRY(write(fd_, &data_, sizeof(data_))); } private: int fd_; @@ -151,24 +247,25 @@ class ScopedEventFdLock { }; AdbConnectionState::AdbConnectionState(const std::string& agent_name) - : agent_name_(agent_name), - controller_(this), - ddm_callback_(this), - sleep_event_fd_(-1), - control_ctx_(nullptr, adbconnection_client_destroy), - local_agent_control_sock_(-1), - remote_agent_control_sock_(-1), - adb_connection_socket_(-1), - adb_write_event_fd_(-1), - shutting_down_(false), - agent_loaded_(false), - agent_listening_(false), - agent_has_socket_(false), - sent_agent_fds_(false), - performed_handshake_(false), - notified_ddm_active_(false), - next_ddm_id_(1), - started_debugger_threads_(false) { + : agent_name_(agent_name), + controller_(this), + ddm_callback_(this), + appinfo_callback_(this), + sleep_event_fd_(-1), + control_ctx_(nullptr, adbconnection_client_destroy), + local_agent_control_sock_(-1), + remote_agent_control_sock_(-1), + adb_connection_socket_(-1), + adb_write_event_fd_(-1), + shutting_down_(false), + agent_loaded_(false), + agent_listening_(false), + agent_has_socket_(false), + sent_agent_fds_(false), + performed_handshake_(false), + notified_ddm_active_(false), + next_ddm_id_(1), + started_debugger_threads_(false) { // Add the startup callback. art::ScopedObjectAccess soa(art::Thread::Current()); art::Runtime::Current()->GetRuntimeCallbacks()->AddDebuggerControlCallback(&controller_); @@ -200,8 +297,10 @@ static art::ObjPtr<art::mirror::Object> CreateAdbConnectionThread(art::Thread* s art::Handle<art::mirror::Object> system_thread_group = hs.NewHandle( system_thread_group_field->GetDeclaringClass()->GetFieldObject<art::mirror::Object>( system_thread_group_field->GetOffset())); - return art::WellKnownClasses::java_lang_Thread_init->NewObject<'L', 'L', 'I', 'Z'>( - hs, self, system_thread_group, thr_name, /*priority=*/ 0, /*daemon=*/ true).Get(); + return art::WellKnownClasses::java_lang_Thread_init + ->NewObject<'L', 'L', 'I', 'Z'>( + hs, self, system_thread_group, thr_name, /*priority=*/0, /*daemon=*/true) + .Get(); } struct CallbackData { @@ -211,9 +310,7 @@ struct CallbackData { static void* CallbackFunction(void* vdata) { std::unique_ptr<CallbackData> data(reinterpret_cast<CallbackData*>(vdata)); - art::Thread* self = art::Thread::Attach(kAdbConnectionThreadName, - true, - data->thr_); + art::Thread* self = art::Thread::Attach(kAdbConnectionThreadName, true, data->thr_); CHECK(self != nullptr) << "threads_being_born_ should have ensured thread could be attached."; // The name in Attach() is only for logging. Set the thread name. This is important so // that the thread is no longer seen as starting up. @@ -254,6 +351,7 @@ void AdbConnectionState::StartDebuggerThreads() { { art::ScopedObjectAccess soa(art::Thread::Current()); art::Runtime::Current()->GetRuntimeCallbacks()->AddDdmCallback(&ddm_callback_); + art::Runtime::Current()->GetRuntimeCallbacks()->AddAppInfoCallback(&appinfo_callback_); } // Setup the socketpair we use to talk to the agent. bool has_sockets; @@ -288,13 +386,11 @@ void AdbConnectionState::StartDebuggerThreads() { } // Note: Using pthreads instead of std::thread to not abort when the thread cannot be // created (exception support required). - std::unique_ptr<CallbackData> data(new CallbackData { this, thr }); + std::unique_ptr<CallbackData> data(new CallbackData{this, thr}); started_debugger_threads_ = true; gPthread.emplace(); - int pthread_create_result = pthread_create(&gPthread.value(), - nullptr, - &CallbackFunction, - data.get()); + int pthread_create_result = + pthread_create(&gPthread.value(), nullptr, &CallbackFunction, data.get()); if (pthread_create_result != 0) { gPthread.reset(); started_debugger_threads_ = false; @@ -309,9 +405,7 @@ void AdbConnectionState::StartDebuggerThreads() { data.release(); // NOLINT pthreads API. } -static bool FlagsSet(int16_t data, int16_t flags) { - return (data & flags) == flags; -} +static bool FlagsSet(int16_t data, int16_t flags) { return (data & flags) == flags; } void AdbConnectionState::CloseFds() { { @@ -351,6 +445,41 @@ void AdbConnectionState::PublishDdmData(uint32_t type, const art::ArrayRef<const SendDdmPacket(NextDdmId(), DdmPacketType::kCmd, type, data); } +void AdbConnectionState::SetCurrentProcessName(const std::string& process_name) { + DCHECK(IsDebuggableOrProfilable()); + VLOG(jdwp) << "SetCurrentProcessName '" << process_name << "'"; + apex_adbconnection_client_set_current_process_name(process_name.c_str()); + WakeupPollLoop(); +} + +void AdbConnectionState::AddApplication(const std::string& package_name) { + DCHECK(IsDebuggableOrProfilable()); + VLOG(jdwp) << "AddApplication'" << package_name << "'"; + apex_adbconnection_client_add_application(package_name.c_str()); + WakeupPollLoop(); +} + +void AdbConnectionState::RemoveApplication(const std::string& package_name) { + DCHECK(IsDebuggableOrProfilable()); + VLOG(jdwp) << "RemoveApplication'" << package_name << "'"; + apex_adbconnection_client_remove_application(package_name.c_str()); + WakeupPollLoop(); +} + +void AdbConnectionState::SetWaitingForDebugger(bool waiting) { + DCHECK(IsDebuggableOrProfilable()); + VLOG(jdwp) << "SetWaitingForDebugger'" << waiting << "'"; + apex_adbconnection_client_set_waiting_for_debugger(waiting); + WakeupPollLoop(); +} + +void AdbConnectionState::SetUserId(int user_id) { + DCHECK(IsDebuggableOrProfilable()); + VLOG(jdwp) << "SetUserId'" << user_id << "'"; + apex_adbconnection_client_set_user_id(user_id); + WakeupPollLoop(); +} + void AdbConnectionState::SendDdmPacket(uint32_t id, DdmPacketType packet_type, uint32_t type, @@ -533,7 +662,7 @@ bool AdbConnectionState::SetupAdbConnection() { } void AdbConnectionState::RunPollLoop(art::Thread* self) { - DCHECK(IsDebuggingPossible() || art::Runtime::Current()->IsProfileableFromShell()); + DCHECK(IsDebuggableOrProfilable()); CHECK_NE(agent_name_, ""); CHECK_EQ(self->GetState(), art::ThreadState::kNative); art::Locks::mutator_lock_->AssertNotHeld(self); @@ -547,6 +676,13 @@ void AdbConnectionState::RunPollLoop(art::Thread* self) { } while (!shutting_down_ && control_ctx_) { bool should_listen_on_connection = !agent_has_socket_ && !sent_agent_fds_; + // By default we are always interested in read and hangup events on the control ctx. + int16_t interestingControlEventSet = POLLIN | POLLRDHUP; + if (apex_adbconnection_client_has_pending_update()) { + // If we have an update for ADBd, we also want to know when the control ctx + // socket is writable. + interestingControlEventSet |= POLLOUT; + } struct pollfd pollfds[4] = { {sleep_event_fd_, POLLIN, 0}, // -1 as an fd causes it to be ignored by poll @@ -554,7 +690,7 @@ void AdbConnectionState::RunPollLoop(art::Thread* self) { // Check for the control_sock_ actually going away. We always monitor for POLLIN, even if // we already have an adbd socket. This allows to reject incoming debugger connection if // there is already have one connected. - {adbconnection_client_pollfd(control_ctx_.get()), POLLIN | POLLRDHUP, 0}, + {adbconnection_client_pollfd(control_ctx_.get()), interestingControlEventSet, 0}, // if we have not loaded the agent either the adb_connection_socket_ is -1 meaning we // don't have a real connection yet or the socket through adb needs to be listened to for // incoming data that the agent or this plugin can handle. @@ -564,8 +700,9 @@ void AdbConnectionState::RunPollLoop(art::Thread* self) { PLOG(ERROR) << "Failed to poll!"; return; } - // We don't actually care about doing this we just use it to wake us up. - // const struct pollfd& sleep_event_poll = pollfds[0]; + VLOG(jdwp) << "adbconnection poll awakening"; + + const struct pollfd& sleep_event_poll = pollfds[0]; const struct pollfd& agent_control_sock_poll = pollfds[1]; const struct pollfd& control_sock_poll = pollfds[2]; const struct pollfd& adb_socket_poll = pollfds[3]; @@ -652,12 +789,19 @@ void AdbConnectionState::RunPollLoop(art::Thread* self) { } else if (agent_listening_ && !sent_agent_fds_) { VLOG(jdwp) << "Sending agent fds again on data."; // Agent was already loaded so it can deal with the handshake. - SendAgentFds(/*require_handshake=*/ true); + SendAgentFds(/*require_handshake=*/true); } + } else if (FlagsSet(control_sock_poll.revents, POLLOUT)) { + VLOG(jdwp) << "Sending state update to adbd"; + apex_adbconnection_client_send_update(control_ctx_.get()); } else if (FlagsSet(adb_socket_poll.revents, POLLRDHUP)) { CHECK(IsDebuggingPossible()); // This path is unexpected for a profileable process. DCHECK(!agent_has_socket_); CloseFds(); + } else if (FlagsSet(sleep_event_poll.revents, POLLIN)) { + // Poll was awakened via fdevent, we need to decrease fdevent counter to prevent poll from + // triggering again. + AcknowledgeWakeup(); } else { VLOG(jdwp) << "Woke up poll without anything to do!"; } @@ -916,19 +1060,36 @@ std::string AdbConnectionState::MakeAgentArg() { return agent_name_ + "=" + parameters.join(); } -void AdbConnectionState::StopDebuggerThreads() { - // The regular agent system will take care of unloading the agent (if needed). - shutting_down_ = true; - // Wakeup the poll loop. +void AdbConnectionState::WakeupPollLoop() { + if (!control_ctx_) { + return; + } + uint64_t data = 1; if (sleep_event_fd_ != -1) { TEMP_FAILURE_RETRY(write(sleep_event_fd_, &data, sizeof(data))); } } +void AdbConnectionState::AcknowledgeWakeup() { + uint64_t data; + if (sleep_event_fd_ != -1) { + TEMP_FAILURE_RETRY(read(sleep_event_fd_, &data, sizeof(data))); + } +} + +void AdbConnectionState::StopDebuggerThreads() { + // The regular agent system will take care of unloading the agent (if needed). + shutting_down_ = true; + WakeupPollLoop(); +} + // The plugin initialization function. extern "C" bool ArtPlugin_Initialize() { DCHECK(art::Runtime::Current()->GetJdwpProvider() == art::JdwpProvider::kAdbConnection); + + RetrieveApexPointers(); + // TODO Provide some way for apps to set this maybe? gState.emplace(kDefaultJdwpAgentName); return ValidateJdwpOptions(art::Runtime::Current()->GetJdwpOptions()); diff --git a/adbconnection/adbconnection.h b/adbconnection/adbconnection.h index b6309e735d..d1d1e51067 100644 --- a/adbconnection/adbconnection.h +++ b/adbconnection/adbconnection.h @@ -24,6 +24,7 @@ #include <limits> #include <memory> +#include <string> #include <vector> #include "adbconnection/client.h" @@ -69,6 +70,21 @@ struct AdbConnectionDdmCallback : public art::DdmCallback { AdbConnectionState* connection_; }; +struct AdbConnectionAppInfoCallback : public art::AppInfoCallback { + explicit AdbConnectionAppInfoCallback(AdbConnectionState* connection) : connection_(connection) {} + + void AddApplication(const std::string& package_name) REQUIRES_SHARED(art::Locks::mutator_lock_); + void RemoveApplication(const std::string& package_name) + REQUIRES_SHARED(art::Locks::mutator_lock_); + void SetCurrentProcessName(const std::string& process_name) + REQUIRES_SHARED(art::Locks::mutator_lock_); + void SetWaitingForDebugger(bool waiting) REQUIRES_SHARED(art::Locks::mutator_lock_); + void SetUserId(int uid) REQUIRES_SHARED(art::Locks::mutator_lock_); + + private: + AdbConnectionState* connection_; +}; + class AdbConnectionState { public: explicit AdbConnectionState(const std::string& name); @@ -82,6 +98,15 @@ class AdbConnectionState { // hand-shaking yet. void PublishDdmData(uint32_t type, const art::ArrayRef<const uint8_t>& data); + // Wake up the poll. Call this if the set of interesting event has changed. They will be + // recalculated and poll will be called again via fdevent write. This wakeup relies on fdevent + // and should be ACKed via AcknowledgeWakeup. + void WakeupPollLoop(); + + // After a call to WakeupPollLoop, the fdevent internal counter should be decreased via + // this method. This should be called after WakeupPollLoop was called and poll triggered. + void AcknowledgeWakeup(); + // Stops debugger threads during shutdown. void StopDebuggerThreads(); @@ -90,6 +115,22 @@ class AdbConnectionState { return started_debugger_threads_; } + // Should be called by Framework when it changes its process name. + void SetCurrentProcessName(const std::string& process_name); + + // Should be called by Framework when it adds an app to a process. + // This can be called several times (see android:process) + void AddApplication(const std::string& package_name); + + // Should be called by Framework when it removes an app from a process. + void RemoveApplication(const std::string& package_name); + + // Should be called by Framework when its debugging state changes. + void SetWaitingForDebugger(bool waiting); + + // Should be called by Framework when the UserID is known. + void SetUserId(int uid); + private: uint32_t NextDdmId(); @@ -121,6 +162,7 @@ class AdbConnectionState { AdbConnectionDebuggerController controller_; AdbConnectionDdmCallback ddm_callback_; + AdbConnectionAppInfoCallback appinfo_callback_; // Eventfd used to allow the StopDebuggerThreads function to wake up sleeping threads android::base::unique_fd sleep_event_fd_; diff --git a/artd/artd.cc b/artd/artd.cc index d87588bbc9..2a0fee8efd 100644 --- a/artd/artd.cc +++ b/artd/artd.cc @@ -51,6 +51,7 @@ #include "android-base/errors.h" #include "android-base/file.h" #include "android-base/logging.h" +#include "android-base/parseint.h" #include "android-base/result.h" #include "android-base/scopeguard.h" #include "android-base/strings.h" @@ -113,10 +114,13 @@ using ::android::base::ErrnoError; using ::android::base::Error; using ::android::base::Join; using ::android::base::make_scope_guard; +using ::android::base::ParseInt; using ::android::base::ReadFileToString; using ::android::base::Result; using ::android::base::Split; +using ::android::base::StartsWith; using ::android::base::Tokenize; +using ::android::base::Trim; using ::android::base::WriteStringToFd; using ::android::base::WriteStringToFile; using ::android::fs_mgr::FstabEntry; @@ -158,6 +162,16 @@ std::optional<int64_t> GetSize(std::string_view path) { return size; } +bool DeleteFile(const std::string& path) { + std::error_code ec; + std::filesystem::remove(path, ec); + if (ec) { + LOG(ERROR) << ART_FORMAT("Failed to remove '{}': {}", path, ec.message()); + return false; + } + return true; +} + // Deletes a file. Returns the size of the deleted file, or 0 if the deleted file is empty or an // error occurs. int64_t GetSizeAndDeleteFile(const std::string& path) { @@ -165,13 +179,9 @@ int64_t GetSizeAndDeleteFile(const std::string& path) { if (!size.has_value()) { return 0; } - - std::error_code ec; - if (!std::filesystem::remove(path, ec)) { - LOG(ERROR) << ART_FORMAT("Failed to remove '{}': {}", path, ec.message()); + if (!DeleteFile(path)) { return 0; } - return size.value(); } @@ -762,15 +772,9 @@ ndk::ScopedAStatus Artd::commitTmpProfile(const TmpProfilePath& in_profile) { } ndk::ScopedAStatus Artd::deleteProfile(const ProfilePath& in_profile) { - RETURN_FATAL_IF_ARG_IS_PRE_REBOOT(in_profile, "profile"); - + // `in_profile` can be either a Pre-reboot path or an ordinary one. std::string profile_path = OR_RETURN_FATAL(BuildProfileOrDmPath(in_profile)); - - std::error_code ec; - std::filesystem::remove(profile_path, ec); - if (ec) { - LOG(ERROR) << ART_FORMAT("Failed to remove '{}': {}", profile_path, ec.message()); - } + DeleteFile(profile_path); return ScopedAStatus::ok(); } @@ -785,7 +789,7 @@ ndk::ScopedAStatus Artd::getProfileVisibility(const ProfilePath& in_profile, ndk::ScopedAStatus Artd::getArtifactsVisibility(const ArtifactsPath& in_artifactsPath, FileVisibility* _aidl_return) { - RETURN_FATAL_IF_ARG_IS_PRE_REBOOT(in_artifactsPath, "artifactsPath"); + // `in_artifactsPath` can be either a Pre-reboot path or an ordinary one. std::string oat_path = OR_RETURN_FATAL(BuildArtifactsPath(in_artifactsPath)).oat_path; *_aidl_return = OR_RETURN_NON_FATAL(GetFileVisibility(oat_path)); return ScopedAStatus::ok(); @@ -1167,36 +1171,15 @@ ndk::ScopedAStatus Artd::dexopt( LOG(INFO) << "Running dex2oat: " << Join(art_exec_args.Get(), /*separator=*/" ") << "\nOpened FDs: " << fd_logger; - ExecCallbacks callbacks{ - .on_start = - [&](pid_t pid) { - std::lock_guard<std::mutex> lock(cancellation_signal->mu_); - cancellation_signal->pids_.insert(pid); - // Handle cancellation signals sent before the process starts. - if (cancellation_signal->is_cancelled_) { - int res = kill_(pid, SIGKILL); - DCHECK_EQ(res, 0); - } - }, - .on_end = - [&](pid_t pid) { - std::lock_guard<std::mutex> lock(cancellation_signal->mu_); - // The pid should no longer receive kill signals sent by `cancellation_signal`. - cancellation_signal->pids_.erase(pid); - }, - }; - ProcessStat stat; - Result<int> result = ExecAndReturnCode(art_exec_args.Get(), kLongTimeoutSec, callbacks, &stat); + Result<int> result = ExecAndReturnCode( + art_exec_args.Get(), kLongTimeoutSec, cancellation_signal->CreateExecCallbacks(), &stat); _aidl_return->wallTimeMs = stat.wall_time_ms; _aidl_return->cpuTimeMs = stat.cpu_time_ms; if (!result.ok()) { - { - std::lock_guard<std::mutex> lock(cancellation_signal->mu_); - if (cancellation_signal->is_cancelled_) { - _aidl_return->cancelled = true; - return ScopedAStatus::ok(); - } + if (cancellation_signal->IsCancelled()) { + _aidl_return->cancelled = true; + return ScopedAStatus::ok(); } return NonFatal("Failed to run dex2oat: " + result.error().message()); } @@ -1238,6 +1221,32 @@ ScopedAStatus ArtdCancellationSignal::getType(int64_t* _aidl_return) { return ScopedAStatus::ok(); } +ExecCallbacks ArtdCancellationSignal::CreateExecCallbacks() { + return { + .on_start = + [&](pid_t pid) { + std::lock_guard<std::mutex> lock(mu_); + pids_.insert(pid); + // Handle cancellation signals sent before the process starts. + if (is_cancelled_) { + int res = kill_(pid, SIGKILL); + DCHECK_EQ(res, 0); + } + }, + .on_end = + [&](pid_t pid) { + std::lock_guard<std::mutex> lock(mu_); + // The pid should no longer receive kill signals sent by `cancellation_signal`. + pids_.erase(pid); + }, + }; +} + +bool ArtdCancellationSignal::IsCancelled() { + std::lock_guard<std::mutex> lock(mu_); + return is_cancelled_; +} + ScopedAStatus Artd::createCancellationSignal( std::shared_ptr<IArtdCancellationSignal>* _aidl_return) { *_aidl_return = ndk::SharedRefBase::make<ArtdCancellationSignal>(kill_); @@ -1286,6 +1295,19 @@ ScopedAStatus Artd::cleanup(const std::vector<ProfilePath>& in_profilesToKeep, return ScopedAStatus::ok(); } +ScopedAStatus Artd::cleanUpPreRebootStagedFiles() { + RETURN_FATAL_IF_PRE_REBOOT(options_); + std::string android_data = OR_RETURN_NON_FATAL(GetAndroidDataOrError()); + std::string android_expand = OR_RETURN_NON_FATAL(GetAndroidExpandOrError()); + for (const std::string& file : ListManagedFiles(android_data, android_expand)) { + if (IsPreRebootStagedFile(file)) { + LOG(INFO) << ART_FORMAT("Cleaning up obsolete Pre-reboot staged file '{}'", file); + DeleteFile(file); + } + } + return ScopedAStatus::ok(); +} + ScopedAStatus Artd::isInDalvikCache(const std::string& in_dexFile, bool* _aidl_return) { // The artifacts should be in the global dalvik-cache directory if: // (1). the dex file is on a system partition, even if the partition is remounted read-write, @@ -1372,9 +1394,9 @@ ScopedAStatus Artd::getProfileSize(const ProfilePath& in_profile, int64_t* _aidl return ScopedAStatus::ok(); } -ScopedAStatus Artd::commitPreRebootStagedFiles( - const std::vector<ArtifactsPath>& in_artifacts, - const std::vector<WritableProfilePath>& in_profiles) { +ScopedAStatus Artd::commitPreRebootStagedFiles(const std::vector<ArtifactsPath>& in_artifacts, + const std::vector<WritableProfilePath>& in_profiles, + bool* _aidl_return) { RETURN_FATAL_IF_PRE_REBOOT(options_); std::vector<std::pair<std::string, std::string>> files_to_move; @@ -1424,6 +1446,40 @@ ScopedAStatus Artd::commitPreRebootStagedFiles( LOG(INFO) << ART_FORMAT("Committed Pre-reboot staged file '{}' to '{}'", src_path, dst_path); } + *_aidl_return = !files_to_move.empty(); + return ScopedAStatus::ok(); +} + +ScopedAStatus Artd::checkPreRebootSystemRequirements(const std::string& in_chrootDir, + bool* _aidl_return) { + RETURN_FATAL_IF_PRE_REBOOT(options_); + BuildSystemProperties new_props = + OR_RETURN_NON_FATAL(BuildSystemProperties::Create(in_chrootDir + "/system/build.prop")); + std::string old_release_str = props_->GetOrEmpty("ro.build.version.release"); + int old_release; + if (!ParseInt(old_release_str, &old_release)) { + return NonFatal( + ART_FORMAT("Failed to read or parse old release number, got '{}'", old_release_str)); + } + std::string new_release_str = new_props.GetOrEmpty("ro.build.version.release"); + int new_release; + if (!ParseInt(new_release_str, &new_release)) { + return NonFatal( + ART_FORMAT("Failed to read or parse new release number, got '{}'", new_release_str)); + } + if (new_release - old_release >= 2) { + // When the release version difference is large, there is no particular technical reason why we + // can't run Pre-reboot Dexopt, but we cannot test and support those cases. + LOG(WARNING) << ART_FORMAT( + "Pre-reboot Dexopt not supported due to large difference in release versions (old_release: " + "{}, new_release: {})", + old_release, + new_release); + *_aidl_return = false; + return ScopedAStatus::ok(); + } + + *_aidl_return = true; return ScopedAStatus::ok(); } @@ -1697,7 +1753,8 @@ Result<void> Artd::BindMount(const std::string& source, const std::string& targe return {}; } -ScopedAStatus Artd::preRebootInit() { +ScopedAStatus Artd::preRebootInit( + const std::shared_ptr<IArtdCancellationSignal>& in_cancellationSignal, bool* _aidl_return) { RETURN_FATAL_IF_NOT_PRE_REBOOT(options_); std::string tmp_dir = pre_reboot_tmp_dir_.value_or(kDefaultPreRebootTmpDir); @@ -1715,7 +1772,8 @@ ScopedAStatus Artd::preRebootInit() { return NonFatal(ART_FORMAT("Failed to check dir '{}': {}", tmp_dir, ec.message())); } if (!is_empty) { - return Fatal("preRebootInit must not be concurrently called or retried after failure"); + return Fatal( + "preRebootInit must not be concurrently called or retried after cancellation or failure"); } } @@ -1729,7 +1787,12 @@ ScopedAStatus Artd::preRebootInit() { if (!preparation_done) { OR_RETURN_NON_FATAL(BindMountNewDir(art_apex_data_dir, GetArtApexData())); OR_RETURN_NON_FATAL(BindMountNewDir(odrefresh_dir, "/data/misc/odrefresh")); - OR_RETURN_NON_FATAL(PreRebootInitBootImages()); + ArtdCancellationSignal* cancellation_signal = + OR_RETURN_FATAL(ToArtdCancellationSignal(in_cancellationSignal.get())); + if (!OR_RETURN_NON_FATAL(PreRebootInitBootImages(cancellation_signal))) { + *_aidl_return = false; + return ScopedAStatus::ok(); + } } if (!preparation_done) { @@ -1739,6 +1802,7 @@ ScopedAStatus Artd::preRebootInit() { } } + *_aidl_return = true; return ScopedAStatus::ok(); } @@ -1812,7 +1876,7 @@ Result<void> Artd::PreRebootInitDeriveClasspath(const std::string& path) { return {}; } -Result<void> Artd::PreRebootInitBootImages() { +Result<bool> Artd::PreRebootInitBootImages(ArtdCancellationSignal* cancellation_signal) { CmdlineBuilder args = OR_RETURN(GetArtExecCmdlineBuilder()); args.Add("--") .Add(OR_RETURN(BuildArtBinPath("odrefresh"))) @@ -1821,8 +1885,12 @@ Result<void> Artd::PreRebootInitBootImages() { LOG(INFO) << "Running odrefresh: " << Join(args.Get(), /*separator=*/" "); - Result<int> result = ExecAndReturnCode(args.Get(), kLongTimeoutSec); + Result<int> result = + ExecAndReturnCode(args.Get(), kLongTimeoutSec, cancellation_signal->CreateExecCallbacks()); if (!result.ok()) { + if (cancellation_signal->IsCancelled()) { + return false; + } return Errorf("Failed to run odrefresh: {}", result.error().message()); } @@ -1833,7 +1901,7 @@ Result<void> Artd::PreRebootInitBootImages() { return Errorf("odrefresh returned an unexpected code: {}", result.value()); } - return {}; + return true; } ScopedAStatus Artd::validateDexPath(const std::string& in_dexFile, @@ -1860,5 +1928,37 @@ ScopedAStatus Artd::validateClassLoaderContext(const std::string& in_dexFile, return ScopedAStatus::ok(); } +Result<BuildSystemProperties> BuildSystemProperties::Create(const std::string& filename) { + std::string content; + if (!ReadFileToString(filename, &content)) { + return ErrnoErrorf("Failed to read '{}'", filename); + } + std::unordered_map<std::string, std::string> system_properties; + for (const std::string& raw_line : Split(content, "\n")) { + std::string line = Trim(raw_line); + if (line.empty() || StartsWith(line, '#')) { + continue; + } + size_t pos = line.find('='); + if (pos == std::string::npos || pos == 0 || (pos == 1 && line[1] == '?')) { + return Errorf("Malformed system property line '{}' in file '{}'", line, filename); + } + if (line[pos - 1] == '?') { + std::string key = line.substr(/*pos=*/0, /*n=*/pos - 1); + if (system_properties.find(key) == system_properties.end()) { + system_properties[key] = line.substr(pos + 1); + } + } else { + system_properties[line.substr(/*pos=*/0, /*n=*/pos)] = line.substr(pos + 1); + } + } + return BuildSystemProperties(std::move(system_properties)); +} + +std::string BuildSystemProperties::GetProperty(const std::string& key) const { + auto it = system_properties_.find(key); + return it != system_properties_.end() ? it->second : ""; +} + } // namespace artd } // namespace art diff --git a/artd/artd.h b/artd/artd.h index a926dbc652..0733f098be 100644 --- a/artd/artd.h +++ b/artd/artd.h @@ -69,6 +69,12 @@ class ArtdCancellationSignal : public aidl::com::android::server::art::BnArtdCan ndk::ScopedAStatus getType(int64_t* _aidl_return) override; + // Returns callbacks to be provided to `ExecUtils`, to register/unregister the process with this + // cancellation signal. + ExecCallbacks CreateExecCallbacks(); + + bool IsCancelled(); + private: std::mutex mu_; // True if cancellation has been signaled. @@ -77,8 +83,6 @@ class ArtdCancellationSignal : public aidl::com::android::server::art::BnArtdCan std::unordered_set<pid_t> pids_ GUARDED_BY(mu_); std::function<int(pid_t, int)> kill_; - - friend class Artd; }; class Artd : public aidl::com::android::server::art::BnArtd { @@ -202,6 +206,8 @@ class Artd : public aidl::com::android::server::art::BnArtd { bool in_keepPreRebootStagedFiles, int64_t* _aidl_return) override; + ndk::ScopedAStatus cleanUpPreRebootStagedFiles() override; + ndk::ScopedAStatus isInDalvikCache(const std::string& in_dexFile, bool* _aidl_return) override; ndk::ScopedAStatus deleteRuntimeArtifacts( @@ -225,9 +231,16 @@ class Artd : public aidl::com::android::server::art::BnArtd { ndk::ScopedAStatus commitPreRebootStagedFiles( const std::vector<aidl::com::android::server::art::ArtifactsPath>& in_artifacts, const std::vector<aidl::com::android::server::art::ProfilePath::WritableProfilePath>& - in_profiles) override; + in_profiles, + bool* _aidl_return) override; + + ndk::ScopedAStatus checkPreRebootSystemRequirements(const std::string& in_chrootDir, + bool* _aidl_return) override; - ndk::ScopedAStatus preRebootInit() override; + ndk::ScopedAStatus preRebootInit( + const std::shared_ptr<aidl::com::android::server::art::IArtdCancellationSignal>& + in_cancellationSignal, + bool* _aidl_return) override; ndk::ScopedAStatus validateDexPath(const std::string& in_dexFile, std::optional<std::string>* _aidl_return) override; @@ -300,7 +313,7 @@ class Artd : public aidl::com::android::server::art::BnArtd { android::base::Result<void> PreRebootInitClearEnvs(); android::base::Result<void> PreRebootInitSetEnvFromFile(const std::string& path); android::base::Result<void> PreRebootInitDeriveClasspath(const std::string& path); - android::base::Result<void> PreRebootInitBootImages(); + android::base::Result<bool> PreRebootInitBootImages(ArtdCancellationSignal* cancellation_signal); std::mutex cache_mu_; std::optional<std::vector<std::string>> cached_boot_image_locations_ GUARDED_BY(cache_mu_); @@ -328,6 +341,23 @@ class Artd : public aidl::com::android::server::art::BnArtd { const std::optional<std::string> init_environ_rc_path_; }; +// A class for getting system properties from a `build.prop` file. +class BuildSystemProperties : public tools::SystemProperties { + public: + // Creates an instance and loads system properties from the `build.prop` file specified at the + // given path. + static android::base::Result<BuildSystemProperties> Create(const std::string& filename); + + protected: + std::string GetProperty(const std::string& key) const override; + + private: + explicit BuildSystemProperties(std::unordered_map<std::string, std::string>&& system_properties) + : system_properties_(std::move(system_properties)) {} + + const std::unordered_map<std::string, std::string> system_properties_; +}; + } // namespace artd } // namespace art diff --git a/artd/artd_test.cc b/artd/artd_test.cc index 1c71ab2fbd..86a7e1d750 100644 --- a/artd/artd_test.cc +++ b/artd/artd_test.cc @@ -2096,9 +2096,7 @@ TEST_F(ArtdTest, mergeProfilesWithOptionsDumpClassesAndMethods) { class ArtdCleanupTest : public ArtdTest { protected: - void SetUp() override { - ArtdTest::SetUp(); - + void SetUpForCleanup() { // Unmanaged files. CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/1.odex"); CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/1.odex"); @@ -2236,6 +2234,7 @@ class ArtdCleanupTest : public ArtdTest { }; TEST_F(ArtdCleanupTest, cleanupKeepingPreRebootStagedFiles) { + SetUpForCleanup(); CreateGcKeptFile( android_expand_ + "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.odex.staged"); @@ -2246,6 +2245,7 @@ TEST_F(ArtdCleanupTest, cleanupKeepingPreRebootStagedFiles) { } TEST_F(ArtdCleanupTest, cleanupRemovingPreRebootStagedFiles) { + SetUpForCleanup(); CreateGcRemovedFile( android_expand_ + "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.odex.staged"); @@ -2255,6 +2255,28 @@ TEST_F(ArtdCleanupTest, cleanupRemovingPreRebootStagedFiles) { Verify(); } +TEST_F(ArtdCleanupTest, cleanUpPreRebootStagedFiles) { + // Unmanaged file. + CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/1.odex.staged"); + + // Not Pre-reboot staged files. + CreateGcKeptFile(android_data_ + "/misc/profiles/ref/com.android.foo/primary.prof"); + CreateGcKeptFile( + android_expand_ + + "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.odex"); + CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.odex"); + + // Pre-reboot staged files. + CreateGcRemovedFile(android_data_ + "/misc/profiles/ref/com.android.foo/primary.prof.staged"); + CreateGcRemovedFile( + android_expand_ + + "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.odex.staged"); + CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.odex.staged"); + + ASSERT_STATUS_OK(artd_->cleanUpPreRebootStagedFiles()); + Verify(); +} + TEST_F(ArtdTest, isInDalvikCache) { TEST_DISABLED_FOR_HOST(); @@ -2509,6 +2531,7 @@ TEST_F(ArtdTest, commitPreRebootStagedFiles) { CreateFile(android_data_ + "/misc/profiles/ref/com.android.bar/primary.prof", "old_prof_2"); + bool aidl_return; ASSERT_STATUS_OK(artd_->commitPreRebootStagedFiles( { // Has all new files. All old files should be replaced. @@ -2529,7 +2552,9 @@ TEST_F(ArtdTest, commitPreRebootStagedFiles) { PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = "primary"}, // Has no new file. PrimaryRefProfilePath{.packageName = "com.android.bar", .profileName = "primary"}, - })); + }, + &aidl_return)); + EXPECT_TRUE(aidl_return); CheckContent(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.dex", "new_odex_1"); @@ -2565,6 +2590,62 @@ TEST_F(ArtdTest, commitPreRebootStagedFiles) { "/misc/profiles/ref/com.android.foo/primary.prof.staged")); } +TEST_F(ArtdTest, commitPreRebootStagedFilesNoNewFile) { + bool aidl_return; + ASSERT_STATUS_OK(artd_->commitPreRebootStagedFiles( + { + ArtifactsPath{.dexPath = android_data_ + "/app/com.android.foo/base.apk", + .isa = "arm", + .isInDalvikCache = false}, + }, + {}, + &aidl_return)); + EXPECT_FALSE(aidl_return); +} + +TEST_F(ArtdTest, checkPreRebootSystemRequirements) { + EXPECT_CALL(*mock_props_, GetProperty("ro.build.version.release")).WillRepeatedly(Return("15")); + std::string chroot_dir = scratch_path_ + "/chroot"; + bool aidl_return; + + constexpr const char* kTemplate = R"( + # Comment. + unrelated.system.property=abc + + ro.build.version.release={} + )"; + + CreateFile(chroot_dir + "/system/build.prop", ART_FORMAT(kTemplate, 15)); + ASSERT_STATUS_OK(artd_->checkPreRebootSystemRequirements(chroot_dir, &aidl_return)); + EXPECT_TRUE(aidl_return); + + CreateFile(chroot_dir + "/system/build.prop", ART_FORMAT(kTemplate, 16)); + ASSERT_STATUS_OK(artd_->checkPreRebootSystemRequirements(chroot_dir, &aidl_return)); + EXPECT_TRUE(aidl_return); + + CreateFile(chroot_dir + "/system/build.prop", ART_FORMAT(kTemplate, 17)); + ASSERT_STATUS_OK(artd_->checkPreRebootSystemRequirements(chroot_dir, &aidl_return)); + EXPECT_FALSE(aidl_return); +} + +TEST_F(ArtdTest, BuildSystemProperties) { + constexpr const char* kContent = R"( + # Comment. + property.foo=123 + property.foo?=456 + property.bar?=000 + property.bar=789 + property.baz?=111 + )"; + + CreateFile(scratch_path_ + "/build.prop", kContent); + BuildSystemProperties props = + OR_FAIL(BuildSystemProperties::Create(scratch_path_ + "/build.prop")); + EXPECT_EQ(props.GetOrEmpty("property.foo"), "123"); + EXPECT_EQ(props.GetOrEmpty("property.bar"), "789"); + EXPECT_EQ(props.GetOrEmpty("property.baz"), "111"); +} + class ArtdPreRebootTest : public ArtdTest { protected: void SetUp() override { @@ -2663,7 +2744,12 @@ TEST_F(ArtdPreRebootTest, preRebootInit) { _)) .WillOnce(Return(0)); - ASSERT_STATUS_OK(artd_->preRebootInit()); + std::shared_ptr<IArtdCancellationSignal> cancellation_signal; + ASSERT_STATUS_OK(artd_->createCancellationSignal(&cancellation_signal)); + + bool aidl_return; + ASSERT_STATUS_OK(artd_->preRebootInit(cancellation_signal, &aidl_return)); + EXPECT_TRUE(aidl_return); auto env_var_count = []() { int count = 0; @@ -2686,7 +2772,8 @@ TEST_F(ArtdPreRebootTest, preRebootInit) { // Calling again will not involve `mount`, `derive_classpath`, or `odrefresh` but only restore env // vars. - EXPECT_TRUE(artd_->preRebootInit().isOk()); + ASSERT_STATUS_OK(artd_->preRebootInit(/*in_cancellationSignal=*/nullptr, &aidl_return)); + EXPECT_TRUE(aidl_return); EXPECT_EQ(getenv("ANDROID_ART_ROOT"), art_root_); EXPECT_EQ(getenv("ANDROID_DATA"), android_data_); EXPECT_STREQ(getenv("BOOTCLASSPATH"), "/foo:/bar"); @@ -2704,7 +2791,11 @@ TEST_F(ArtdPreRebootTest, preRebootInitFailed) { EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(Contains(art_root_ + "/bin/odrefresh"), _, _)) .WillOnce(Return(1)); - ndk::ScopedAStatus status = artd_->preRebootInit(); + std::shared_ptr<IArtdCancellationSignal> cancellation_signal; + ASSERT_STATUS_OK(artd_->createCancellationSignal(&cancellation_signal)); + + bool aidl_return; + ndk::ScopedAStatus status = artd_->preRebootInit(cancellation_signal, &aidl_return); EXPECT_FALSE(status.isOk()); EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC); EXPECT_STREQ(status.getMessage(), "odrefresh returned an unexpected code: 1"); @@ -2714,11 +2805,66 @@ TEST_F(ArtdPreRebootTest, preRebootInitNoRetry) { // Simulate that a previous attempt failed halfway. ASSERT_TRUE(WriteStringToFile("", pre_reboot_tmp_dir_ + "/classpath.txt")); - ndk::ScopedAStatus status = artd_->preRebootInit(); + bool aidl_return; + ndk::ScopedAStatus status = artd_->preRebootInit(/*in_cancellationSignal=*/nullptr, &aidl_return); EXPECT_FALSE(status.isOk()); EXPECT_EQ(status.getExceptionCode(), EX_ILLEGAL_STATE); - EXPECT_STREQ(status.getMessage(), - "preRebootInit must not be concurrently called or retried after failure"); + EXPECT_STREQ( + status.getMessage(), + "preRebootInit must not be concurrently called or retried after cancellation or failure"); +} + +TEST_F(ArtdPreRebootTest, preRebootInitCancelled) { + EXPECT_CALL(*mock_exec_utils_, + DoExecAndReturnCode(Contains("/apex/com.android.sdkext/bin/derive_classpath"), _, _)) + .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("/proc/self/fd/", "export BOOTCLASSPATH /foo:/bar")), + Return(0))); + + EXPECT_CALL(mock_mount_, Call).Times(2).WillRepeatedly(Return(0)); + + std::shared_ptr<IArtdCancellationSignal> cancellation_signal; + ASSERT_STATUS_OK(artd_->createCancellationSignal(&cancellation_signal)); + + constexpr pid_t kPid = 123; + constexpr std::chrono::duration<int> kTimeout = std::chrono::seconds(1); + + std::condition_variable process_started_cv, process_killed_cv; + std::mutex mu; + + EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(Contains(art_root_ + "/bin/odrefresh"), _, _)) + .WillOnce([&](auto, const ExecCallbacks& callbacks, auto) { + std::unique_lock<std::mutex> lock(mu); + // Step 2. + callbacks.on_start(kPid); + process_started_cv.notify_one(); + EXPECT_EQ(process_killed_cv.wait_for(lock, kTimeout), std::cv_status::no_timeout); + // Step 5. + callbacks.on_end(kPid); + return Error(); + }); + + EXPECT_CALL(mock_kill_, Call(kPid, SIGKILL)).WillOnce([&](auto, auto) { + // Step 4. + process_killed_cv.notify_one(); + return 0; + }); + + std::thread t; + bool aidl_return; + { + std::unique_lock<std::mutex> lock(mu); + // Step 1. + t = std::thread( + [&] { ASSERT_STATUS_OK(artd_->preRebootInit(cancellation_signal, &aidl_return)); }); + EXPECT_EQ(process_started_cv.wait_for(lock, kTimeout), std::cv_status::no_timeout); + // Step 3. + cancellation_signal->cancel(); + } + + t.join(); + + // Step 6. + EXPECT_FALSE(aidl_return); } TEST_F(ArtdPreRebootTest, dexopt) { diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl index 54f55f8460..47fc89966b 100644 --- a/artd/binder/com/android/server/art/IArtd.aidl +++ b/artd/binder/com/android/server/art/IArtd.aidl @@ -192,6 +192,15 @@ interface IArtd { boolean keepPreRebootStagedFiles); /** + * Deletes all Pre-reboot staged files. + * + * Not supported in Pre-reboot Dexopt mode. + * + * Throws fatal errors. Logs and ignores non-fatal errors. + */ + void cleanUpPreRebootStagedFiles(); + + /** * Returns whether the artifacts of the primary dex files should be in the global dalvik-cache * directory. * @@ -263,24 +272,47 @@ interface IArtd { * Not supported in Pre-reboot Dexopt mode. * * Throws fatal and non-fatal errors. + * + * @return true if any file has been committed. */ - void commitPreRebootStagedFiles( + boolean commitPreRebootStagedFiles( in List<com.android.server.art.ArtifactsPath> artifacts, in List<com.android.server.art.ProfilePath.WritableProfilePath> profiles); + /** + * Returns whether the old system and the new system meet the requirements to run Pre-reboot + * Dexopt. This method can only be called with a chroot dir set up by + * {@link IDexoptChrootSetup#setUp}. + * + * Not supported in Pre-reboot Dexopt mode. + * + * Throws fatal and non-fatal errors. + */ + boolean checkPreRebootSystemRequirements(@utf8InCpp String chrootDir); + // The methods below are only for Pre-reboot Dexopt and only supported in Pre-reboot Dexopt // mode. /** * Initializes the environment for Pre-reboot Dexopt. This operation includes initializing - * environment variables and boot images. + * environment variables and boot images. Returns true on success, or false on cancellation. + * Throws on failure. * * Note that this method results in a non-persistent state change, so it must be called every * time a new instance of artd is started for Pre-reboot Dexopt. * + * On the first call to this method, a cancellation signal must be passed through the {@code + * cancellationSignal} parameter. The cancellation signal can then be used for cancelling the + * first call. On subsequent calls to this method, the {@code cancellationSignal} parameter is + * ignored. + * + * After cancellation or failure, the environment will not be usable for Pre-reboot Dexopt, and + * this operation cannot be retried. + * * Throws fatal and non-fatal errors. */ - void preRebootInit(); + boolean preRebootInit( + in @nullable com.android.server.art.IArtdCancellationSignal cancellationSignal); /** For Pre-reboot Dexopt use. See {@link ArtJni#validateDexPath}. */ @nullable @utf8InCpp String validateDexPath(@utf8InCpp String dexFile); diff --git a/build/apex/art_apex_test.py b/build/apex/art_apex_test.py index e5b72ee4a7..05cc1a6314 100755 --- a/build/apex/art_apex_test.py +++ b/build/apex/art_apex_test.py @@ -569,7 +569,6 @@ class ReleaseTargetChecker: self._checker.check_native_library('libandroidio') # Check internal native library dependencies. - self._checker.check_native_library('libcrypto') self._checker.check_native_library('libexpat') diff --git a/cmdline/cmdline_parser_test.cc b/cmdline/cmdline_parser_test.cc index 93218bfe32..e586dd4292 100644 --- a/cmdline/cmdline_parser_test.cc +++ b/cmdline/cmdline_parser_test.cc @@ -38,7 +38,6 @@ namespace art { return lhs.enabled_ == rhs.enabled_ && lhs.min_save_period_ms_ == rhs.min_save_period_ms_ && lhs.save_resolved_classes_delay_ms_ == rhs.save_resolved_classes_delay_ms_ && - lhs.hot_startup_method_samples_ == rhs.hot_startup_method_samples_ && lhs.min_methods_to_save_ == rhs.min_methods_to_save_ && lhs.min_classes_to_save_ == rhs.min_classes_to_save_ && lhs.min_notification_before_wake_ == rhs.min_notification_before_wake_ && @@ -237,6 +236,9 @@ TEST_F(CmdlineParserTest, TestSimpleSuccesses) { EXPECT_SINGLE_PARSE_VALUE(false, "-XX:DisableHSpaceCompactForOOM", M::EnableHSpaceCompactForOOM); EXPECT_SINGLE_PARSE_VALUE(0.5, "-XX:HeapTargetUtilization=0.5", M::HeapTargetUtilization); EXPECT_SINGLE_PARSE_VALUE(5u, "-XX:ParallelGCThreads=5", M::ParallelGCThreads); + EXPECT_SINGLE_PARSE_VALUE(10u, + "-XX:ParallelGCThreads=5 -XX:ParallelGCThreads=10", + M::ParallelGCThreads); } // TEST_F TEST_F(CmdlineParserTest, TestSimpleFailures) { @@ -487,19 +489,18 @@ TEST_F(CmdlineParserTest, TestJitOptions) { * -Xps-* */ TEST_F(CmdlineParserTest, ProfileSaverOptions) { - ProfileSaverOptions opt = ProfileSaverOptions(true, 1, 2, 3, 4, 5, 6, 7, 8, 9, "abc", true); + ProfileSaverOptions opt = ProfileSaverOptions(true, 1, 2, 3, 4, 5, 6, 7, 8, "abc", true); EXPECT_SINGLE_PARSE_VALUE(opt, "-Xjitsaveprofilinginfo " "-Xps-min-save-period-ms:1 " "-Xps-min-first-save-ms:2 " "-Xps-save-resolved-classes-delay-ms:3 " - "-Xps-hot-startup-method-samples:4 " - "-Xps-min-methods-to-save:5 " - "-Xps-min-classes-to-save:6 " - "-Xps-min-notification-before-wake:7 " - "-Xps-max-notification-before-wake:8 " - "-Xps-inline-cache-threshold:9 " + "-Xps-min-methods-to-save:4 " + "-Xps-min-classes-to-save:5 " + "-Xps-min-notification-before-wake:6 " + "-Xps-max-notification-before-wake:7 " + "-Xps-inline-cache-threshold:8 " "-Xps-profile-path:abc " "-Xps-profile-boot-class-path", M::ProfileSaverOpts); diff --git a/cmdline/cmdline_types.h b/cmdline/cmdline_types.h index 7cacfde12a..34c1b0fc97 100644 --- a/cmdline/cmdline_types.h +++ b/cmdline/cmdline_types.h @@ -832,10 +832,8 @@ struct CmdlineType<ProfileSaverOptions> : CmdlineTypeParser<ProfileSaverOptions> type_parser.Parse(suffix)); } if (android::base::StartsWith(option, "hot-startup-method-samples:")) { - CmdlineType<unsigned int> type_parser; - return ParseInto(existing, - &ProfileSaverOptions::hot_startup_method_samples_, - type_parser.Parse(suffix)); + LOG(WARNING) << "-Xps-hot-startup-method-samples option is deprecated"; + return Result::SuccessNoValue(); } if (android::base::StartsWith(option, "min-methods-to-save:")) { CmdlineType<unsigned int> type_parser; diff --git a/compiler/driver/compiler_options.cc b/compiler/driver/compiler_options.cc index 87dbfee412..8b415c641d 100644 --- a/compiler/driver/compiler_options.cc +++ b/compiler/driver/compiler_options.cc @@ -135,8 +135,8 @@ bool CompilerOptions::IsImageClass(const char* descriptor) const { return image_classes_.find(std::string_view(descriptor)) != image_classes_.end(); } -bool CompilerOptions::IsPreloadedClass(const char* pretty_descriptor) const { - return preloaded_classes_.find(std::string_view(pretty_descriptor)) != preloaded_classes_.end(); +bool CompilerOptions::IsPreloadedClass(std::string_view pretty_descriptor) const { + return preloaded_classes_.find(pretty_descriptor) != preloaded_classes_.end(); } bool CompilerOptions::ShouldCompileWithClinitCheck(ArtMethod* method) const { diff --git a/compiler/driver/compiler_options.h b/compiler/driver/compiler_options.h index abdb01b372..36ecf88199 100644 --- a/compiler/driver/compiler_options.h +++ b/compiler/driver/compiler_options.h @@ -292,7 +292,7 @@ class CompilerOptions final { // Returns whether the given `pretty_descriptor` is in the list of preloaded // classes. `pretty_descriptor` should be the result of calling `PrettyDescriptor`. - EXPORT bool IsPreloadedClass(const char* pretty_descriptor) const; + EXPORT bool IsPreloadedClass(std::string_view pretty_descriptor) const; bool ParseCompilerOptions(const std::vector<std::string>& options, bool ignore_unrecognized, diff --git a/compiler/jit/jit_compiler.cc b/compiler/jit/jit_compiler.cc index 051368cc8a..4b2f8d2e14 100644 --- a/compiler/jit/jit_compiler.cc +++ b/compiler/jit/jit_compiler.cc @@ -178,7 +178,8 @@ bool JitCompiler::CompileMethod( Thread* self, JitMemoryRegion* region, ArtMethod* method, CompilationKind compilation_kind) { SCOPED_TRACE << "JIT compiling " << method->PrettyMethod() - << " (kind=" << compilation_kind << ")"; + << " (kind=" << compilation_kind << ")" + << " from " << method->GetDexFile()->GetLocation(); DCHECK(!method->IsProxyMethod()); DCHECK(method->GetDeclaringClass()->IsResolved()); diff --git a/compiler/jni/jni_compiler_test.cc b/compiler/jni/jni_compiler_test.cc index 77ebff6da2..8a096d0744 100644 --- a/compiler/jni/jni_compiler_test.cc +++ b/compiler/jni/jni_compiler_test.cc @@ -14,11 +14,11 @@ * limitations under the License. */ +#include <math.h> + #include <memory> #include <type_traits> -#include <math.h> - #include "art_method-inl.h" #include "base/bit_utils.h" #include "base/casts.h" @@ -40,7 +40,7 @@ #include "mirror/object-inl.h" #include "mirror/object_array-inl.h" #include "mirror/stack_trace_element-inl.h" -#include "nativehelper/ScopedLocalRef.h" +#include "nativehelper/scoped_local_ref.h" #include "nativeloader/native_loader.h" #include "oat/oat_quick_method_header.h" #include "runtime.h" diff --git a/compiler/optimizing/code_generator.h b/compiler/optimizing/code_generator.h index 3ac4bd7dcc..aec7b45a1a 100644 --- a/compiler/optimizing/code_generator.h +++ b/compiler/optimizing/code_generator.h @@ -59,15 +59,15 @@ static int32_t constexpr kPrimIntMax = 0x7fffffff; // Maximum value for a primitive long. static int64_t constexpr kPrimLongMax = INT64_C(0x7fffffffffffffff); -constexpr size_t status_lsb_position = SubtypeCheckBits::BitStructSizeOf(); -constexpr size_t status_byte_offset = - mirror::Class::StatusOffset().SizeValue() + (status_lsb_position / kBitsPerByte); -constexpr uint32_t shifted_visibly_initialized_value = - enum_cast<uint32_t>(ClassStatus::kVisiblyInitialized) << (status_lsb_position % kBitsPerByte); -constexpr uint32_t shifted_initializing_value = - enum_cast<uint32_t>(ClassStatus::kInitializing) << (status_lsb_position % kBitsPerByte); -constexpr uint32_t shifted_initialized_value = - enum_cast<uint32_t>(ClassStatus::kInitialized) << (status_lsb_position % kBitsPerByte); +constexpr size_t kClassStatusLsbPosition = SubtypeCheckBits::BitStructSizeOf(); +constexpr size_t kClassStatusByteOffset = + mirror::Class::StatusOffset().SizeValue() + (kClassStatusLsbPosition / kBitsPerByte); +constexpr uint32_t kShiftedVisiblyInitializedValue = enum_cast<uint32_t>( + ClassStatus::kVisiblyInitialized) << (kClassStatusLsbPosition % kBitsPerByte); +constexpr uint32_t kShiftedInitializingValue = + enum_cast<uint32_t>(ClassStatus::kInitializing) << (kClassStatusLsbPosition % kBitsPerByte); +constexpr uint32_t kShiftedInitializedValue = + enum_cast<uint32_t>(ClassStatus::kInitialized) << (kClassStatusLsbPosition % kBitsPerByte); class Assembler; class CodeGenerationData; diff --git a/compiler/optimizing/code_generator_arm64.cc b/compiler/optimizing/code_generator_arm64.cc index 988809ee48..226e5343e2 100644 --- a/compiler/optimizing/code_generator_arm64.cc +++ b/compiler/optimizing/code_generator_arm64.cc @@ -1350,18 +1350,18 @@ void CodeGeneratorARM64::GenerateFrameEntry() { // We don't emit a read barrier here to save on code size. We rely on the // resolution trampoline to do a suspend check before re-entering this code. __ Ldr(temp1, MemOperand(kArtMethodRegister, ArtMethod::DeclaringClassOffset().Int32Value())); - __ Ldrb(temp2, HeapOperand(temp1, status_byte_offset)); - __ Cmp(temp2, shifted_visibly_initialized_value); + __ Ldrb(temp2, HeapOperand(temp1, kClassStatusByteOffset)); + __ Cmp(temp2, kShiftedVisiblyInitializedValue); __ B(hs, &frame_entry_label_); // Check if we're initialized and jump to code that does a memory barrier if // so. - __ Cmp(temp2, shifted_initialized_value); + __ Cmp(temp2, kShiftedInitializedValue); __ B(hs, &memory_barrier); // Check if we're initializing and the thread initializing is the one // executing the code. - __ Cmp(temp2, shifted_initializing_value); + __ Cmp(temp2, kShiftedInitializingValue); __ B(lo, &resolution); __ Ldr(temp1, HeapOperand(temp1, mirror::Class::ClinitThreadIdOffset().Int32Value())); @@ -2080,8 +2080,8 @@ void InstructionCodeGeneratorARM64::GenerateClassInitializationCheck(SlowPathCod // size, load only the high byte of the field and compare with 0xf0. // Note: The same code size could be achieved with LDR+MNV(asr #24)+CBNZ but benchmarks // show that this pattern is slower (tested on little cores). - __ Ldrb(temp, HeapOperand(class_reg, status_byte_offset)); - __ Cmp(temp, shifted_visibly_initialized_value); + __ Ldrb(temp, HeapOperand(class_reg, kClassStatusByteOffset)); + __ Cmp(temp, kShiftedVisiblyInitializedValue); __ B(lo, slow_path->GetEntryLabel()); __ Bind(slow_path->GetExitLabel()); } diff --git a/compiler/optimizing/code_generator_arm_vixl.cc b/compiler/optimizing/code_generator_arm_vixl.cc index 7c1a9b21f2..979db6ed75 100644 --- a/compiler/optimizing/code_generator_arm_vixl.cc +++ b/compiler/optimizing/code_generator_arm_vixl.cc @@ -2354,18 +2354,18 @@ void CodeGeneratorARMVIXL::GenerateFrameEntry() { // We don't emit a read barrier here to save on code size. We rely on the // resolution trampoline to do a suspend check before re-entering this code. __ Ldr(temp1, MemOperand(kMethodRegister, ArtMethod::DeclaringClassOffset().Int32Value())); - __ Ldrb(temp2, MemOperand(temp1, status_byte_offset)); - __ Cmp(temp2, shifted_visibly_initialized_value); + __ Ldrb(temp2, MemOperand(temp1, kClassStatusByteOffset)); + __ Cmp(temp2, kShiftedVisiblyInitializedValue); __ B(cs, &frame_entry_label_); // Check if we're initialized and jump to code that does a memory barrier if // so. - __ Cmp(temp2, shifted_initialized_value); + __ Cmp(temp2, kShiftedInitializedValue); __ B(cs, &memory_barrier); // Check if we're initializing and the thread initializing is the one // executing the code. - __ Cmp(temp2, shifted_initializing_value); + __ Cmp(temp2, kShiftedInitializingValue); __ B(lo, &resolution); __ Ldr(temp1, MemOperand(temp1, mirror::Class::ClinitThreadIdOffset().Int32Value())); @@ -7862,8 +7862,8 @@ void InstructionCodeGeneratorARMVIXL::GenerateClassInitializationCheck( LoadClassSlowPathARMVIXL* slow_path, vixl32::Register class_reg) { UseScratchRegisterScope temps(GetVIXLAssembler()); vixl32::Register temp = temps.Acquire(); - __ Ldrb(temp, MemOperand(class_reg, status_byte_offset)); - __ Cmp(temp, shifted_visibly_initialized_value); + __ Ldrb(temp, MemOperand(class_reg, kClassStatusByteOffset)); + __ Cmp(temp, kShiftedVisiblyInitializedValue); __ B(lo, slow_path->GetEntryLabel()); __ Bind(slow_path->GetExitLabel()); } diff --git a/compiler/optimizing/code_generator_riscv64.cc b/compiler/optimizing/code_generator_riscv64.cc index 9b499a08a2..75d5db83a0 100644 --- a/compiler/optimizing/code_generator_riscv64.cc +++ b/compiler/optimizing/code_generator_riscv64.cc @@ -137,8 +137,8 @@ static RegisterSet OneRegInReferenceOutSaveEverythingCallerSaves() { template <ClassStatus kStatus> static constexpr int64_t ShiftedSignExtendedClassStatusValue() { // This is used only for status values that have the highest bit set. - static_assert(CLZ(enum_cast<uint32_t>(kStatus)) == status_lsb_position); - constexpr uint32_t kShiftedStatusValue = enum_cast<uint32_t>(kStatus) << status_lsb_position; + static_assert(CLZ(enum_cast<uint32_t>(kStatus)) == kClassStatusLsbPosition); + constexpr uint32_t kShiftedStatusValue = enum_cast<uint32_t>(kStatus) << kClassStatusLsbPosition; static_assert(kShiftedStatusValue >= 0x80000000u); return static_cast<int64_t>(kShiftedStatusValue) - (INT64_C(1) << 32); } diff --git a/compiler/optimizing/code_generator_x86.cc b/compiler/optimizing/code_generator_x86.cc index 4329e40efc..2f6dde3df7 100644 --- a/compiler/optimizing/code_generator_x86.cc +++ b/compiler/optimizing/code_generator_x86.cc @@ -1390,17 +1390,18 @@ void CodeGeneratorX86::GenerateFrameEntry() { NearLabel continue_execution, resolution; // We'll use EBP as temporary. __ pushl(EBP); + __ cfi().AdjustCFAOffset(4); // Check if we're visibly initialized. // We don't emit a read barrier here to save on code size. We rely on the // resolution trampoline to do a suspend check before re-entering this code. __ movl(EBP, Address(kMethodRegisterArgument, ArtMethod::DeclaringClassOffset().Int32Value())); - __ cmpb(Address(EBP, status_byte_offset), Immediate(shifted_visibly_initialized_value)); + __ cmpb(Address(EBP, kClassStatusByteOffset), Immediate(kShiftedVisiblyInitializedValue)); __ j(kAboveEqual, &continue_execution); // Check if we're initializing and the thread initializing is the one // executing the code. - __ cmpb(Address(EBP, status_byte_offset), Immediate(shifted_initializing_value)); + __ cmpb(Address(EBP, kClassStatusByteOffset), Immediate(kShiftedInitializingValue)); __ j(kBelow, &resolution); __ movl(EBP, Address(EBP, mirror::Class::ClinitThreadIdOffset().Int32Value())); @@ -1409,13 +1410,16 @@ void CodeGeneratorX86::GenerateFrameEntry() { __ Bind(&resolution); __ popl(EBP); + __ cfi().AdjustCFAOffset(-4); // Jump to the resolution stub. ThreadOffset32 entrypoint_offset = GetThreadOffset<kX86PointerSize>(kQuickQuickResolutionTrampoline); __ fs()->jmp(Address::Absolute(entrypoint_offset)); __ Bind(&continue_execution); + __ cfi().AdjustCFAOffset(4); // Undo the `-4` adjustment above. We get here with EBP pushed. __ popl(EBP); + __ cfi().AdjustCFAOffset(-4); } __ Bind(&frame_entry_label_); @@ -7510,7 +7514,7 @@ void InstructionCodeGeneratorX86::VisitClinitCheck(HClinitCheck* check) { void InstructionCodeGeneratorX86::GenerateClassInitializationCheck( SlowPathCode* slow_path, Register class_reg) { - __ cmpb(Address(class_reg, status_byte_offset), Immediate(shifted_visibly_initialized_value)); + __ cmpb(Address(class_reg, kClassStatusByteOffset), Immediate(kShiftedVisiblyInitializedValue)); __ j(kBelow, slow_path->GetEntryLabel()); __ Bind(slow_path->GetExitLabel()); } diff --git a/compiler/optimizing/code_generator_x86_64.cc b/compiler/optimizing/code_generator_x86_64.cc index 5b4f1b8b25..5c9ee8fea4 100644 --- a/compiler/optimizing/code_generator_x86_64.cc +++ b/compiler/optimizing/code_generator_x86_64.cc @@ -1839,13 +1839,14 @@ void CodeGeneratorX86_64::GenerateFrameEntry() { __ movl(CpuRegister(TMP), Address(CpuRegister(kMethodRegisterArgument), ArtMethod::DeclaringClassOffset().Int32Value())); - __ cmpb(Address(CpuRegister(TMP), status_byte_offset), - Immediate(shifted_visibly_initialized_value)); + __ cmpb(Address(CpuRegister(TMP), kClassStatusByteOffset), + Immediate(kShiftedVisiblyInitializedValue)); __ j(kAboveEqual, &frame_entry_label_); // Check if we're initializing and the thread initializing is the one // executing the code. - __ cmpb(Address(CpuRegister(TMP), status_byte_offset), Immediate(shifted_initializing_value)); + __ cmpb(Address(CpuRegister(TMP), kClassStatusByteOffset), + Immediate(kShiftedInitializingValue)); __ j(kBelow, &resolution); __ movl(CpuRegister(TMP), @@ -6596,7 +6597,7 @@ void ParallelMoveResolverX86_64::RestoreScratch(int reg) { void InstructionCodeGeneratorX86_64::GenerateClassInitializationCheck( SlowPathCode* slow_path, CpuRegister class_reg) { - __ cmpb(Address(class_reg, status_byte_offset), Immediate(shifted_visibly_initialized_value)); + __ cmpb(Address(class_reg, kClassStatusByteOffset), Immediate(kShiftedVisiblyInitializedValue)); __ j(kBelow, slow_path->GetEntryLabel()); __ Bind(slow_path->GetExitLabel()); } diff --git a/compiler/optimizing/loop_optimization.cc b/compiler/optimizing/loop_optimization.cc index 14e6683cf8..8d698d6c82 100644 --- a/compiler/optimizing/loop_optimization.cc +++ b/compiler/optimizing/loop_optimization.cc @@ -532,7 +532,7 @@ HLoopOptimization::HLoopOptimization(HGraph* graph, vector_permanent_map_(nullptr), vector_external_set_(nullptr), predicate_info_map_(nullptr), - vector_mode_(VectorMode::kSequential), + synthesis_mode_(LoopSynthesisMode::kSequential), vector_preheader_(nullptr), vector_header_(nullptr), vector_body_(nullptr), @@ -970,10 +970,10 @@ bool HLoopOptimization::TryVectorizePredicated(LoopNode* node, } bool HLoopOptimization::TryVectorizedTraditional(LoopNode* node, - HBasicBlock* body, - HBasicBlock* exit, - HPhi* main_phi, - int64_t trip_count) { + HBasicBlock* body, + HBasicBlock* exit, + HPhi* main_phi, + int64_t trip_count) { HBasicBlock* header = node->loop_info->GetHeader(); size_t num_of_blocks = header->GetLoopInformation()->GetBlocks().NumSetBits(); @@ -1283,6 +1283,8 @@ void HLoopOptimization::VectorizePredicated(LoopNode* node, HBasicBlock* exit) { DCHECK(IsInPredicatedVectorizationMode()); + vector_external_set_->clear(); + HBasicBlock* header = node->loop_info->GetHeader(); HBasicBlock* preheader = node->loop_info->GetPreHeader(); @@ -1322,7 +1324,7 @@ void HLoopOptimization::VectorizePredicated(LoopNode* node, // <vectorized-loop-body> HBasicBlock* preheader_for_vector_loop = graph_->TransformLoopForVectorization(vector_header_, vector_body_, exit); - vector_mode_ = VectorMode::kVector; + synthesis_mode_ = LoopSynthesisMode::kVector; GenerateNewLoopPredicated(node, preheader_for_vector_loop, vector_index_, @@ -1333,7 +1335,7 @@ void HLoopOptimization::VectorizePredicated(LoopNode* node, // for ( ; i < stc; i += 1) // <loop-body> if (needs_disambiguation_test) { - vector_mode_ = VectorMode::kSequential; + synthesis_mode_ = LoopSynthesisMode::kSequential; HBasicBlock* preheader_for_cleanup_loop = graph_->TransformLoopForVectorization(vector_header_, vector_body_, exit); // Use "Traditional" version for the sequential loop. @@ -1369,6 +1371,8 @@ void HLoopOptimization::VectorizeTraditional(LoopNode* node, int64_t trip_count) { DCHECK(!IsInPredicatedVectorizationMode()); + vector_external_set_->clear(); + HBasicBlock* header = node->loop_info->GetHeader(); HBasicBlock* preheader = node->loop_info->GetPreHeader(); @@ -1472,7 +1476,7 @@ void HLoopOptimization::VectorizeTraditional(LoopNode* node, // moved around during suspend checks, since all analysis was based on // nothing more than the Android runtime alignment conventions. if (ptc != nullptr) { - vector_mode_ = VectorMode::kSequential; + synthesis_mode_ = LoopSynthesisMode::kSequential; HBasicBlock* preheader_for_peeling_loop = graph_->TransformLoopForVectorization(vector_header_, vector_body_, exit); GenerateNewLoopScalarOrTraditional(node, @@ -1486,7 +1490,7 @@ void HLoopOptimization::VectorizeTraditional(LoopNode* node, // Generate vector loop, possibly further unrolled: // for ( ; i < vtc; i += chunk) // <vectorized-loop-body> - vector_mode_ = VectorMode::kVector; + synthesis_mode_ = LoopSynthesisMode::kVector; HBasicBlock* preheader_for_vector_loop = graph_->TransformLoopForVectorization(vector_header_, vector_body_, exit); GenerateNewLoopScalarOrTraditional(node, @@ -1500,7 +1504,7 @@ void HLoopOptimization::VectorizeTraditional(LoopNode* node, // for ( ; i < stc; i += 1) // <loop-body> if (needs_cleanup) { - vector_mode_ = VectorMode::kSequential; + synthesis_mode_ = LoopSynthesisMode::kSequential; HBasicBlock* preheader_for_cleanup_loop = graph_->TransformLoopForVectorization(vector_header_, vector_body_, exit); GenerateNewLoopScalarOrTraditional(node, @@ -1566,7 +1570,6 @@ HPhi* HLoopOptimization::InitializeForNewLoop(HBasicBlock* new_preheader, HInstr vector_header_->AddPhi(phi); vector_index_ = phi; vector_permanent_map_->clear(); - vector_external_set_->clear(); predicate_info_map_->clear(); return phi; @@ -1578,7 +1581,7 @@ void HLoopOptimization::GenerateNewLoopScalarOrTraditional(LoopNode* node, HInstruction* hi, HInstruction* step, uint32_t unroll) { - DCHECK(unroll == 1 || vector_mode_ == VectorMode::kVector); + DCHECK(unroll == 1 || synthesis_mode_ == LoopSynthesisMode::kVector); DataType::Type induc_type = lo->GetType(); HPhi* phi = InitializeForNewLoop(new_preheader, lo); @@ -1600,7 +1603,7 @@ void HLoopOptimization::GenerateNewLoopPredicated(LoopNode* node, HInstruction* hi, HInstruction* step) { DCHECK(IsInPredicatedVectorizationMode()); - DCHECK_EQ(vector_mode_, VectorMode::kVector); + DCHECK(synthesis_mode_ == LoopSynthesisMode::kVector); DataType::Type induc_type = lo->GetType(); HPhi* phi = InitializeForNewLoop(new_preheader, lo); @@ -1887,7 +1890,7 @@ bool HLoopOptimization::VectorizeUse(LoopNode* node, size_from >= size_vec && VectorizeUse(node, opa, generate_code, type, restrictions))) { if (generate_code) { - if (vector_mode_ == VectorMode::kVector) { + if (synthesis_mode_ == LoopSynthesisMode::kVector) { vector_map_->Put(instruction, vector_map_->Get(opa)); // operand pass-through } else { GenerateVecOp(instruction, vector_map_->Get(opa), nullptr, type); @@ -1963,7 +1966,7 @@ bool HLoopOptimization::VectorizeUse(LoopNode* node, // Accept shift operator for vectorizable/invariant operands. // TODO: accept symbolic, albeit loop invariant shift factors. DCHECK(r != nullptr); - if (generate_code && vector_mode_ != VectorMode::kVector) { // de-idiom + if (generate_code && synthesis_mode_ != LoopSynthesisMode::kVector) { // de-idiom r = opa; } int64_t distance = 0; @@ -1991,7 +1994,7 @@ bool HLoopOptimization::VectorizeUse(LoopNode* node, } // Accept ABS(x) for vectorizable operand. DCHECK(r != nullptr); - if (generate_code && vector_mode_ != VectorMode::kVector) { // de-idiom + if (generate_code && synthesis_mode_ != LoopSynthesisMode::kVector) { // de-idiom r = opa; } if (VectorizeUse(node, r, generate_code, type, restrictions)) { @@ -2187,7 +2190,7 @@ void HLoopOptimization::GenerateVecInv(HInstruction* org, DataType::Type type) { if (vector_map_->find(org) == vector_map_->end()) { // In scalar code, just use a self pass-through for scalar invariants // (viz. expression remains itself). - if (vector_mode_ == VectorMode::kSequential) { + if (synthesis_mode_ == LoopSynthesisMode::kSequential) { vector_map_->Put(org, org); return; } @@ -2208,7 +2211,7 @@ void HLoopOptimization::GenerateVecInv(HInstruction* org, DataType::Type type) { vector = new (global_allocator_) HVecReplicateScalar(global_allocator_, input, type, vector_length_, kNoDexPc); vector_permanent_map_->Put(org, Insert(vector_preheader_, vector)); - vector_external_set_->insert(vector); + MaybeInsertInVectorExternalSet(vector); } vector_map_->Put(org, vector); } @@ -2235,7 +2238,7 @@ void HLoopOptimization::GenerateVecMem(HInstruction* org, DataType::Type type) { uint32_t dex_pc = org->GetDexPc(); HInstruction* vector = nullptr; - if (vector_mode_ == VectorMode::kVector) { + if (synthesis_mode_ == LoopSynthesisMode::kVector) { // Vector store or load. bool is_string_char_at = false; HInstruction* base = org->InputAt(0); @@ -2267,7 +2270,7 @@ void HLoopOptimization::GenerateVecMem(HInstruction* org, } } else { // Scalar store or load. - DCHECK(vector_mode_ == VectorMode::kSequential); + DCHECK(synthesis_mode_ == LoopSynthesisMode::kSequential); if (opb != nullptr) { DataType::Type component_type = org->AsArraySet()->GetComponentType(); vector = new (global_allocator_) HArraySet( @@ -2285,7 +2288,7 @@ void HLoopOptimization::GenerateVecReductionPhi(HPhi* orig_phi) { DCHECK(reductions_->find(orig_phi) != reductions_->end()); DCHECK(reductions_->Get(orig_phi->InputAt(1)) == orig_phi); HInstruction* vector = nullptr; - if (vector_mode_ == VectorMode::kSequential) { + if (synthesis_mode_ == LoopSynthesisMode::kSequential) { HPhi* new_phi = new (global_allocator_) HPhi( global_allocator_, kNoRegNumber, 0, orig_phi->GetType()); vector_header_->AddPhi(new_phi); @@ -2314,7 +2317,7 @@ void HLoopOptimization::GenerateVecReductionPhiInputs(HPhi* phi, HInstruction* r DCHECK(new_phi->IsVecOperation()); } // Prepare the new initialization. - if (vector_mode_ == VectorMode::kVector) { + if (synthesis_mode_ == LoopSynthesisMode::kVector) { // Generate a [initial, 0, .., 0] vector for add or // a [initial, initial, .., initial] vector for min/max. HVecOperation* red_vector = new_red->AsVecOperation(); @@ -2337,7 +2340,7 @@ void HLoopOptimization::GenerateVecReductionPhiInputs(HPhi* phi, HInstruction* r vector_length, kNoDexPc)); } - vector_external_set_->insert(new_init); + MaybeInsertInVectorExternalSet(new_init); } else { new_init = ReduceAndExtractIfNeeded(new_init); } @@ -2366,24 +2369,24 @@ HInstruction* HLoopOptimization::ReduceAndExtractIfNeeded(HInstruction* instruct HVecReduce* reduce = new (global_allocator_) HVecReduce( global_allocator_, instruction, type, vector_length, kind, kNoDexPc); exit->InsertInstructionBefore(reduce, exit->GetFirstInstruction()); - vector_external_set_->insert(reduce); + MaybeInsertInVectorExternalSet(reduce); instruction = new (global_allocator_) HVecExtractScalar( global_allocator_, reduce, type, vector_length, 0, kNoDexPc); exit->InsertInstructionAfter(instruction, reduce); - vector_external_set_->insert(instruction); + MaybeInsertInVectorExternalSet(instruction); } } return instruction; } -#define GENERATE_VEC(x, y) \ - if (vector_mode_ == VectorMode::kVector) { \ - vector = (x); \ - } else { \ - DCHECK(vector_mode_ == VectorMode::kSequential); \ - vector = (y); \ - } \ +#define GENERATE_VEC(x, y) \ + if (synthesis_mode_ == LoopSynthesisMode::kVector) { \ + vector = (x); \ + } else { \ + DCHECK(synthesis_mode_ == LoopSynthesisMode::kSequential); \ + vector = (y); \ + } \ break; HInstruction* HLoopOptimization::GenerateVecOp(HInstruction* org, @@ -2461,7 +2464,7 @@ HInstruction* HLoopOptimization::GenerateVecOp(HInstruction* org, new (global_allocator_) HAbs(org_type, opa, dex_pc)); case HInstruction::kEqual: { // Special case. - DCHECK_EQ(vector_mode_, VectorMode::kVector); + DCHECK_EQ(synthesis_mode_, LoopSynthesisMode::kVector); vector = new (global_allocator_) HVecCondition(global_allocator_, opa, opb, type, vector_length_, dex_pc); } @@ -2527,14 +2530,14 @@ bool HLoopOptimization::VectorizeHalvingAddIdiom(LoopNode* node, // Accept recognized halving add for vectorizable operands. Vectorized code uses the // shorthand idiomatic operation. Sequential code uses the original scalar expressions. DCHECK(r != nullptr && s != nullptr); - if (generate_code && vector_mode_ != VectorMode::kVector) { // de-idiom + if (generate_code && synthesis_mode_ != LoopSynthesisMode::kVector) { // de-idiom r = instruction->InputAt(0); s = instruction->InputAt(1); } if (VectorizeUse(node, r, generate_code, type, restrictions) && VectorizeUse(node, s, generate_code, type, restrictions)) { if (generate_code) { - if (vector_mode_ == VectorMode::kVector) { + if (synthesis_mode_ == LoopSynthesisMode::kVector) { vector_map_->Put(instruction, new (global_allocator_) HVecHalvingAdd( global_allocator_, vector_map_->Get(r), @@ -2603,14 +2606,14 @@ bool HLoopOptimization::VectorizeSADIdiom(LoopNode* node, // Accept SAD idiom for vectorizable operands. Vectorized code uses the shorthand // idiomatic operation. Sequential code uses the original scalar expressions. DCHECK(r != nullptr && s != nullptr); - if (generate_code && vector_mode_ != VectorMode::kVector) { // de-idiom + if (generate_code && synthesis_mode_ != LoopSynthesisMode::kVector) { // de-idiom r = s = abs->InputAt(0); } if (VectorizeUse(node, acc, generate_code, sub_type, restrictions) && VectorizeUse(node, r, generate_code, sub_type, restrictions) && VectorizeUse(node, s, generate_code, sub_type, restrictions)) { if (generate_code) { - if (vector_mode_ == VectorMode::kVector) { + if (synthesis_mode_ == LoopSynthesisMode::kVector) { vector_map_->Put(instruction, new (global_allocator_) HVecSADAccumulate( global_allocator_, vector_map_->Get(acc), @@ -2676,7 +2679,7 @@ bool HLoopOptimization::VectorizeDotProdIdiom(LoopNode* node, DCHECK(r != nullptr && s != nullptr); // Accept dot product idiom for vectorizable operands. Vectorized code uses the shorthand // idiomatic operation. Sequential code uses the original scalar expressions. - if (generate_code && vector_mode_ != VectorMode::kVector) { // de-idiom + if (generate_code && synthesis_mode_ != LoopSynthesisMode::kVector) { // de-idiom r = mul_left; s = mul_right; } @@ -2684,7 +2687,7 @@ bool HLoopOptimization::VectorizeDotProdIdiom(LoopNode* node, VectorizeUse(node, r, generate_code, op_type, restrictions) && VectorizeUse(node, s, generate_code, op_type, restrictions)) { if (generate_code) { - if (vector_mode_ == VectorMode::kVector) { + if (synthesis_mode_ == LoopSynthesisMode::kVector) { vector_map_->Put(instruction, new (global_allocator_) HVecDotProd( global_allocator_, vector_map_->Get(acc), @@ -2762,7 +2765,7 @@ bool HLoopOptimization::VectorizeIfCondition(LoopNode* node, return false; } - if (generate_code && vector_mode_ != VectorMode::kVector) { // de-idiom + if (generate_code && synthesis_mode_ != LoopSynthesisMode::kVector) { // de-idiom opa_promoted = opa; opb_promoted = opb; } @@ -2774,7 +2777,7 @@ bool HLoopOptimization::VectorizeIfCondition(LoopNode* node, vector_map_->Get(opa_promoted), vector_map_->Get(opb_promoted), type); - DCHECK_EQ(vector_mode_, VectorMode::kVector); + DCHECK_EQ(synthesis_mode_, LoopSynthesisMode::kVector); HInstruction* vec_pred_not = new (global_allocator_) HVecPredNot(global_allocator_, vec_cond, type, vector_length_, hif->GetDexPc()); @@ -3167,11 +3170,17 @@ void HLoopOptimization::InitPredicateInfoMap(LoopNode* node, back_edge_info->SetControlPredicate(header_info->GetTruePredicate()); } -std::ostream& operator<<(std::ostream& os, const HLoopOptimization::VectorMode& mode) { +void HLoopOptimization::MaybeInsertInVectorExternalSet(HInstruction* instruction) { + if (IsInPredicatedVectorizationMode()) { + vector_external_set_->insert(instruction); + } +} + +std::ostream& operator<<(std::ostream& os, const HLoopOptimization::LoopSynthesisMode& mode) { switch (mode) { - case HLoopOptimization::VectorMode::kSequential: + case HLoopOptimization::LoopSynthesisMode::kSequential: return os << "kSequential"; - case HLoopOptimization::VectorMode::kVector: + case HLoopOptimization::LoopSynthesisMode::kVector: return os << "kVector"; } return os; diff --git a/compiler/optimizing/loop_optimization.h b/compiler/optimizing/loop_optimization.h index 4817060dfa..546d42b4fa 100644 --- a/compiler/optimizing/loop_optimization.h +++ b/compiler/optimizing/loop_optimization.h @@ -105,14 +105,14 @@ class HLoopOptimization : public HOptimization { }; /* - * Vectorization mode during synthesis + * Loop synthesis mode during vectorization * (sequential peeling/cleanup loop or vector loop). */ - enum class VectorMode { + enum class LoopSynthesisMode { kSequential, kVector }; - friend std::ostream& operator<<(std::ostream& os, const VectorMode& fd_logger); + friend std::ostream& operator<<(std::ostream& os, const LoopSynthesisMode& fd_logger); /* * Representation of a unit-stride array reference. @@ -480,6 +480,7 @@ class HLoopOptimization : public HOptimization { bool CanRemoveCycle(); // Whether the current 'iset_' is removable. bool IsInPredicatedVectorizationMode() const { return predicated_vectorization_mode_; } + void MaybeInsertInVectorExternalSet(HInstruction* instruction); // Compiler options (to query ISA features). const CompilerOptions* compiler_options_; @@ -547,6 +548,12 @@ class HLoopOptimization : public HOptimization { // Tracks vector operations that are inserted outside of the loop (preheader, exit) // as part of vectorization (e.g. replicate scalar for loop invariants and reduce ops // for loop reductions). + // + // The instructions in the set are live for the whole vectorization process of the current + // loop, not just during generation of a particular loop version (as the sets above). + // + // Currently the set is being only used in the predicated mode - for assigning governing + // predicates. ScopedArenaSet<HInstruction*>* vector_external_set_; // A mapping between a basic block of the original loop and its associated PredicateInfo. @@ -555,7 +562,7 @@ class HLoopOptimization : public HOptimization { ScopedArenaSafeMap<HBasicBlock*, BlockPredicateInfo*>* predicate_info_map_; // Temporary vectorization bookkeeping. - VectorMode vector_mode_; // synthesis mode + LoopSynthesisMode synthesis_mode_; // synthesis mode HBasicBlock* vector_preheader_; // preheader of the new loop HBasicBlock* vector_header_; // header of the new loop HBasicBlock* vector_body_; // body of the new loop diff --git a/compiler/optimizing/nodes.h b/compiler/optimizing/nodes.h index 33ffc07ba8..89369f59f1 100644 --- a/compiler/optimizing/nodes.h +++ b/compiler/optimizing/nodes.h @@ -143,6 +143,8 @@ enum GraphAnalysisResult { kAnalysisSuccess, }; +std::ostream& operator<<(std::ostream& os, GraphAnalysisResult ga); + template <typename T> static inline typename std::make_unsigned<T>::type MakeUnsigned(T x) { return static_cast<typename std::make_unsigned<T>::type>(x); diff --git a/compiler/optimizing/optimizing_compiler.cc b/compiler/optimizing/optimizing_compiler.cc index 45d534a9ec..3f73459a00 100644 --- a/compiler/optimizing/optimizing_compiler.cc +++ b/compiler/optimizing/optimizing_compiler.cc @@ -30,6 +30,7 @@ #include "base/macros.h" #include "base/mutex.h" #include "base/scoped_arena_allocator.h" +#include "base/systrace.h" #include "base/timing_logger.h" #include "builder.h" #include "code_generator.h" @@ -781,6 +782,7 @@ CodeGenerator* OptimizingCompiler::TryCompile(ArenaAllocator* allocator, } if (Compiler::IsPathologicalCase(*code_item, method_idx, dex_file)) { + SCOPED_TRACE << "Not compiling because of pathological case"; MaybeRecordStat(compilation_stats_.get(), MethodCompilationStat::kNotCompiledPathological); return nullptr; } @@ -791,6 +793,7 @@ CodeGenerator* OptimizingCompiler::TryCompile(ArenaAllocator* allocator, if ((compiler_options.GetCompilerFilter() == CompilerFilter::kSpace) && (CodeItemInstructionAccessor(dex_file, code_item).InsnsSizeInCodeUnits() > kSpaceFilterOptimizingThreshold)) { + SCOPED_TRACE << "Not compiling because of space filter"; MaybeRecordStat(compilation_stats_.get(), MethodCompilationStat::kNotCompiledSpaceFilter); return nullptr; } @@ -865,6 +868,7 @@ CodeGenerator* OptimizingCompiler::TryCompile(ArenaAllocator* allocator, compilation_stats_.get()); GraphAnalysisResult result = builder.BuildGraph(); if (result != kAnalysisSuccess) { + SCOPED_TRACE << "Not compiling because of " << result; switch (result) { case kAnalysisSkipped: { MaybeRecordStat(compilation_stats_.get(), @@ -927,6 +931,7 @@ CodeGenerator* OptimizingCompiler::TryCompile(ArenaAllocator* allocator, // However, we may have run out of memory trying to create it, so in this // case just abort the compilation. if (graph->GetProfilingInfo() == nullptr) { + SCOPED_TRACE << "Not compiling because of out of memory"; MaybeRecordStat(compilation_stats_.get(), MethodCompilationStat::kJitOutOfMemoryForCommit); return nullptr; } @@ -938,6 +943,7 @@ CodeGenerator* OptimizingCompiler::TryCompile(ArenaAllocator* allocator, compilation_stats_.get()); if (UNLIKELY(codegen->GetFrameSize() > codegen->GetMaximumFrameSize())) { + SCOPED_TRACE << "Not compiling because of stack frame too large"; LOG(WARNING) << "Stack frame size is " << codegen->GetFrameSize() << " which is larger than the maximum of " << codegen->GetMaximumFrameSize() << " bytes. Method: " << graph->PrettyMethod(); diff --git a/compiler/utils/riscv64/assembler_riscv64.cc b/compiler/utils/riscv64/assembler_riscv64.cc index 9cd4a45f0c..a0832bc3a8 100644 --- a/compiler/utils/riscv64/assembler_riscv64.cc +++ b/compiler/utils/riscv64/assembler_riscv64.cc @@ -6553,6 +6553,13 @@ void Riscv64Assembler::Unimp() { /////////////////////////////// RV64 MACRO Instructions END /////////////////////////////// const Riscv64Assembler::Branch::BranchInfo Riscv64Assembler::Branch::branch_info_[] = { + // Compressed branches (can be promoted to longer) + {2, 0, Riscv64Assembler::Branch::kOffset9}, // kCondCBranch + {2, 0, Riscv64Assembler::Branch::kOffset12}, // kUncondCBranch + // Compressed branches (can't be promoted to longer) + {2, 0, Riscv64Assembler::Branch::kOffset9}, // kBareCondCBranch + {2, 0, Riscv64Assembler::Branch::kOffset12}, // kBareUncondCBranch + // Short branches (can be promoted to longer). {4, 0, Riscv64Assembler::Branch::kOffset13}, // kCondBranch {4, 0, Riscv64Assembler::Branch::kOffset21}, // kUncondBranch @@ -6562,13 +6569,14 @@ const Riscv64Assembler::Branch::BranchInfo Riscv64Assembler::Branch::branch_info {4, 0, Riscv64Assembler::Branch::kOffset21}, // kBareUncondBranch {4, 0, Riscv64Assembler::Branch::kOffset21}, // kBareCall - // Medium branch. + // Medium branches. + {6, 2, Riscv64Assembler::Branch::kOffset21}, // kCondCBranch21 {8, 4, Riscv64Assembler::Branch::kOffset21}, // kCondBranch21 // Long branches. {12, 4, Riscv64Assembler::Branch::kOffset32}, // kLongCondBranch - {8, 0, Riscv64Assembler::Branch::kOffset32}, // kLongUncondBranch - {8, 0, Riscv64Assembler::Branch::kOffset32}, // kLongCall + {8, 0, Riscv64Assembler::Branch::kOffset32}, // kLongUncondBranch + {8, 0, Riscv64Assembler::Branch::kOffset32}, // kLongCall // label. {8, 0, Riscv64Assembler::Branch::kOffset32}, // kLabel @@ -6595,10 +6603,50 @@ void Riscv64Assembler::Branch::InitShortOrLong(Riscv64Assembler::Branch::OffsetB type_ = type; } +void Riscv64Assembler::Branch::InitShortOrLong(Riscv64Assembler::Branch::OffsetBits offset_size, + Riscv64Assembler::Branch::Type compressed_type, + Riscv64Assembler::Branch::Type short_type, + Riscv64Assembler::Branch::Type long_type, + Riscv64Assembler::Branch::Type longest_type) { + Riscv64Assembler::Branch::Type type = compressed_type; + if (offset_size > branch_info_[type].offset_size) { + type = short_type; + if (offset_size > branch_info_[type].offset_size) { + type = long_type; + if (offset_size > branch_info_[type].offset_size) { + type = longest_type; + } + } + } + type_ = type; +} + void Riscv64Assembler::Branch::InitializeType(Type initial_type) { OffsetBits offset_size_needed = GetOffsetSizeNeeded(location_, target_); switch (initial_type) { + case kCondCBranch: + CHECK(IsCompressableCondition()); + if (condition_ != kUncond) { + InitShortOrLong( + offset_size_needed, kCondCBranch, kCondBranch, kCondCBranch21, kLongCondBranch); + break; + } + FALLTHROUGH_INTENDED; + case kUncondCBranch: + InitShortOrLong(offset_size_needed, kUncondCBranch, kUncondBranch, kLongUncondBranch); + break; + case kBareCondCBranch: + if (condition_ != kUncond) { + type_ = kBareCondCBranch; + CHECK_LE(offset_size_needed, GetOffsetSize()); + break; + } + FALLTHROUGH_INTENDED; + case kBareUncondCBranch: + type_ = kBareUncondCBranch; + CHECK_LE(offset_size_needed, GetOffsetSize()); + break; case kCondBranch: if (condition_ != kUncond) { InitShortOrLong(offset_size_needed, kCondBranch, kCondBranch21, kLongCondBranch); @@ -6673,7 +6721,21 @@ bool Riscv64Assembler::Branch::IsUncond(BranchCondition condition, XRegister lhs } } -Riscv64Assembler::Branch::Branch(uint32_t location, uint32_t target, XRegister rd, bool is_bare) +bool Riscv64Assembler::Branch::IsCompressed(Type type) { + switch (type) { + case kCondCBranch: + case kUncondCBranch: + case kBareCondCBranch: + case kBareUncondCBranch: + case kCondCBranch21: + return true; + default: + return false; + } +} + +Riscv64Assembler::Branch::Branch( + uint32_t location, uint32_t target, XRegister rd, bool is_bare, bool compression_allowed) : old_location_(location), location_(location), target_(target), @@ -6681,9 +6743,12 @@ Riscv64Assembler::Branch::Branch(uint32_t location, uint32_t target, XRegister r rhs_reg_(Zero), freg_(kNoFRegister), condition_(kUncond), - next_branch_id_(0u) { - InitializeType( - (rd != Zero ? (is_bare ? kBareCall : kCall) : (is_bare ? kBareUncondBranch : kUncondBranch))); + next_branch_id_(0u), + compression_allowed_(compression_allowed) { + InitializeType((rd != Zero ? + (is_bare ? kBareCall : kCall) : + (is_bare ? (compression_allowed ? kBareUncondCBranch : kBareUncondBranch) : + (compression_allowed ? kUncondCBranch : kUncondBranch)))); } Riscv64Assembler::Branch::Branch(uint32_t location, @@ -6691,7 +6756,8 @@ Riscv64Assembler::Branch::Branch(uint32_t location, Riscv64Assembler::BranchCondition condition, XRegister lhs_reg, XRegister rhs_reg, - bool is_bare) + bool is_bare, + bool compression_allowed) : old_location_(location), location_(location), target_(target), @@ -6703,7 +6769,12 @@ Riscv64Assembler::Branch::Branch(uint32_t location, DCHECK_NE(condition, kUncond); DCHECK(!IsNop(condition, lhs_reg, rhs_reg)); DCHECK(!IsUncond(condition, lhs_reg, rhs_reg)); - InitializeType(is_bare ? kBareCondBranch : kCondBranch); + if (!IsCompressableCondition()) { + compression_allowed = false; + } + compression_allowed_ = compression_allowed; + InitializeType(is_bare ? (compression_allowed ? kBareCondCBranch : kBareCondBranch) : + (compression_allowed ? kCondCBranch : kCondBranch)); } Riscv64Assembler::Branch::Branch(uint32_t location, @@ -6717,7 +6788,8 @@ Riscv64Assembler::Branch::Branch(uint32_t location, rhs_reg_(Zero), freg_(kNoFRegister), condition_(kUncond), - next_branch_id_(0u) { + next_branch_id_(0u), + compression_allowed_(false) { CHECK_NE(rd , Zero); InitializeType(label_or_literal_type); } @@ -6733,7 +6805,8 @@ Riscv64Assembler::Branch::Branch(uint32_t location, rhs_reg_(Zero), freg_(rd), condition_(kUncond), - next_branch_id_(0u) { + next_branch_id_(0u), + compression_allowed_(false) { InitializeType(literal_type); } @@ -6768,6 +6841,8 @@ Riscv64Assembler::BranchCondition Riscv64Assembler::Branch::OppositeCondition( Riscv64Assembler::Branch::Type Riscv64Assembler::Branch::GetType() const { return type_; } +Riscv64Assembler::Branch::Type Riscv64Assembler::Branch::GetOldType() const { return old_type_; } + Riscv64Assembler::BranchCondition Riscv64Assembler::Branch::GetCondition() const { return condition_; } @@ -6776,6 +6851,14 @@ XRegister Riscv64Assembler::Branch::GetLeftRegister() const { return lhs_reg_; } XRegister Riscv64Assembler::Branch::GetRightRegister() const { return rhs_reg_; } +XRegister Riscv64Assembler::Branch::GetNonZeroRegister() const { + DCHECK(GetLeftRegister() == Zero || GetRightRegister() == Zero) + << "Either register has to be Zero register"; + DCHECK(GetLeftRegister() != Zero || GetRightRegister() != Zero) + << "Either register has to be non-Zero register"; + return GetLeftRegister() == Zero ? GetRightRegister() : GetLeftRegister(); +} + FRegister Riscv64Assembler::Branch::GetFRegister() const { return freg_; } uint32_t Riscv64Assembler::Branch::GetTarget() const { return target_; } @@ -6809,6 +6892,11 @@ bool Riscv64Assembler::Branch::IsBare() const { bool Riscv64Assembler::Branch::IsResolved() const { return target_ != kUnresolved; } +bool Riscv64Assembler::Branch::IsCompressableCondition() const { + return (condition_ == kCondEQ || condition_ == kCondNE) && + ((lhs_reg_ == Zero && IsShortReg(rhs_reg_)) || (rhs_reg_ == Zero && IsShortReg(lhs_reg_))); +} + Riscv64Assembler::Branch::OffsetBits Riscv64Assembler::Branch::GetOffsetSize() const { return branch_info_[type_].offset_size; } @@ -6818,10 +6906,15 @@ Riscv64Assembler::Branch::OffsetBits Riscv64Assembler::Branch::GetOffsetSizeNeed // For unresolved targets assume the shortest encoding // (later it will be made longer if needed). if (target == kUnresolved) { - return kOffset13; + return kOffset9; } int64_t distance = static_cast<int64_t>(target) - location; - if (IsInt<kOffset13>(distance)) { + + if (IsInt<kOffset9>(distance)) { + return kOffset9; + } else if (IsInt<kOffset12>(distance)) { + return kOffset12; + } else if (IsInt<kOffset13>(distance)) { return kOffset13; } else if (IsInt<kOffset21>(distance)) { return kOffset21; @@ -6848,23 +6941,52 @@ uint32_t Riscv64Assembler::Branch::PromoteIfNeeded() { DCHECK(IsResolved()); Type old_type = type_; switch (type_) { + // Compressed branches (can be promoted to longer) + case kUncondCBranch: { + OffsetBits needed_size = GetOffsetSizeNeeded(GetOffsetLocation(), target_); + if (needed_size <= GetOffsetSize()) { + return 0u; + } + + type_ = needed_size <= branch_info_[kUncondBranch].offset_size ? kUncondBranch : + kLongUncondBranch; + break; + } + case kCondCBranch: { + DCHECK(IsCompressableCondition()); + OffsetBits needed_size = GetOffsetSizeNeeded(GetOffsetLocation(), target_); + if (needed_size <= GetOffsetSize()) { + return 0u; + } + + if (needed_size <= branch_info_[kCondBranch].offset_size) { + type_ = kCondBranch; + break; + } + FALLTHROUGH_INTENDED; + } // Short branches (can be promoted to longer). case kCondBranch: { OffsetBits needed_size = GetOffsetSizeNeeded(GetOffsetLocation(), target_); if (needed_size <= GetOffsetSize()) { return 0u; } - // The offset remains the same for `kCondBranch21` for forward branches. - DCHECK_EQ(branch_info_[kCondBranch21].length - branch_info_[kCondBranch21].pc_offset, + + Type cond21Type = old_type == kCondCBranch ? kCondCBranch21 : kCondBranch21; + if (compression_allowed_ && cond21Type == kCondBranch21 && IsCompressableCondition()) { + // If this branch was promoted from compressed one on initialization stage + // it could be promoted back to compressed if possible + cond21Type = kCondCBranch21; + } + + // The offset remains the same for `kCond[C]Branch21` for forward branches. + DCHECK_EQ(branch_info_[cond21Type].length - branch_info_[cond21Type].pc_offset, branch_info_[kCondBranch].length - branch_info_[kCondBranch].pc_offset); if (target_ <= location_) { - // Calculate the needed size for kCondBranch21. - needed_size = - GetOffsetSizeNeeded(location_ + branch_info_[kCondBranch21].pc_offset, target_); + // Calculate the needed size for kCond[C]Branch21. + needed_size = GetOffsetSizeNeeded(location_ + branch_info_[cond21Type].pc_offset, target_); } - type_ = (needed_size <= branch_info_[kCondBranch21].offset_size) - ? kCondBranch21 - : kLongCondBranch; + type_ = (needed_size <= branch_info_[cond21Type].offset_size) ? cond21Type : kLongCondBranch; break; } case kUncondBranch: @@ -6880,15 +7002,21 @@ uint32_t Riscv64Assembler::Branch::PromoteIfNeeded() { type_ = kLongCall; break; // Medium branch (can be promoted to long). - case kCondBranch21: - if (GetOffsetSizeNeeded(GetOffsetLocation(), target_) <= GetOffsetSize()) { + case kCondCBranch21: + DCHECK(IsCompressableCondition()); + FALLTHROUGH_INTENDED; + case kCondBranch21: { + OffsetBits needed_size = GetOffsetSizeNeeded(GetOffsetLocation(), target_); + if (needed_size <= GetOffsetSize()) { return 0u; } type_ = kLongCondBranch; break; + } default: // Other branch types cannot be promoted. - DCHECK_LE(GetOffsetSizeNeeded(GetOffsetLocation(), target_), GetOffsetSize()) << type_; + DCHECK_LE(GetOffsetSizeNeeded(GetOffsetLocation(), target_), GetOffsetSize()) + << static_cast<uint32_t>(type_); return 0u; } DCHECK(type_ != old_type); @@ -6947,6 +7075,7 @@ void Riscv64Assembler::EmitBranch(Riscv64Assembler::Branch* branch) { BranchCondition condition = branch->GetCondition(); XRegister lhs = branch->GetLeftRegister(); XRegister rhs = branch->GetRightRegister(); + // Disable Compressed emitter explicitly and enable where it is needed ScopedNoCInstructions no_compression(this); auto emit_auipc_and_next = [&](XRegister reg, auto next) { @@ -6957,6 +7086,26 @@ void Riscv64Assembler::EmitBranch(Riscv64Assembler::Branch* branch) { }; switch (branch->GetType()) { + // Compressed branches + case Branch::kCondCBranch: + case Branch::kBareCondCBranch: { + ScopedUseCInstructions use_compression(this); + CHECK_EQ(overwrite_location_, branch->GetOffsetLocation()); + DCHECK(branch->IsCompressableCondition()); + if (condition == kCondEQ) { + CBeqz(branch->GetNonZeroRegister(), offset); + } else { + CBnez(branch->GetNonZeroRegister(), offset); + } + break; + } + case Branch::kUncondCBranch: + case Branch::kBareUncondCBranch: { + ScopedUseCInstructions use_compression(this); + CHECK_EQ(overwrite_location_, branch->GetOffsetLocation()); + CJ(offset); + break; + } // Short branches. case Branch::kUncondBranch: case Branch::kBareUncondBranch: @@ -6981,7 +7130,22 @@ void Riscv64Assembler::EmitBranch(Riscv64Assembler::Branch* branch) { CHECK_EQ(overwrite_location_, branch->GetOffsetLocation()); J(offset); break; - + case Branch::kCondCBranch21: { + DCHECK(branch->IsCompressableCondition()); + { + ScopedUseCInstructions use_compression(this); + if (condition == kCondNE) { + DCHECK_EQ(Branch::OppositeCondition(condition), kCondEQ); + CBeqz(branch->GetNonZeroRegister(), branch->GetLength()); + } else { + DCHECK_EQ(Branch::OppositeCondition(condition), kCondNE); + CBnez(branch->GetNonZeroRegister(), branch->GetLength()); + } + } + CHECK_EQ(overwrite_location_, branch->GetOffsetLocation()); + J(offset); + break; + } // Long branches. case Branch::kLongCondBranch: EmitBcond(Branch::OppositeCondition(condition), lhs, rhs, branch->GetLength()); @@ -7033,11 +7197,12 @@ void Riscv64Assembler::EmitBranches() { } void Riscv64Assembler::FinalizeLabeledBranch(Riscv64Label* label) { + const uint32_t alignment = + IsExtensionEnabled(Riscv64Extension::kZca) ? sizeof(uint16_t) : sizeof(uint32_t); Branch& this_branch = branches_.back(); - DCHECK_ALIGNED(this_branch.GetLength(), sizeof(uint32_t)); - uint32_t length = this_branch.GetLength() / sizeof(uint32_t); - ScopedNoCInstructions no_compression(this); - + uint32_t branch_length = this_branch.GetLength(); + DCHECK(IsAlignedParam(branch_length, alignment)); + uint32_t length = branch_length / alignment; if (!label->IsBound()) { // Branch forward (to a following label), distance is unknown. // The first branch forward will contain 0, serving as the terminator of @@ -7050,7 +7215,11 @@ void Riscv64Assembler::FinalizeLabeledBranch(Riscv64Label* label) { } // Reserve space for the branch. for (; length != 0u; --length) { - Nop(); + if (alignment == sizeof(uint16_t)) { + Emit16(0); + } else { + Emit32(0); + } } } @@ -7067,13 +7236,20 @@ void Riscv64Assembler::Bcond( } uint32_t target = label->IsBound() ? GetLabelLocation(label) : Branch::kUnresolved; - branches_.emplace_back(buffer_.Size(), target, condition, lhs, rhs, is_bare); + branches_.emplace_back(buffer_.Size(), + target, + condition, + lhs, + rhs, + is_bare, + IsExtensionEnabled(Riscv64Extension::kZca)); FinalizeLabeledBranch(label); } void Riscv64Assembler::Buncond(Riscv64Label* label, XRegister rd, bool is_bare) { uint32_t target = label->IsBound() ? GetLabelLocation(label) : Branch::kUnresolved; - branches_.emplace_back(buffer_.Size(), target, rd, is_bare); + branches_.emplace_back( + buffer_.Size(), target, rd, is_bare, IsExtensionEnabled(Riscv64Extension::kZca)); FinalizeLabeledBranch(label); } @@ -7216,6 +7392,7 @@ void Riscv64Assembler::ReserveJumpTableSpace() { void Riscv64Assembler::PromoteBranches() { // Promote short branches to long as necessary. bool changed; + // To avoid re-computing predicate on each iteration cache it in local do { changed = false; for (auto& branch : branches_) { diff --git a/compiler/utils/riscv64/assembler_riscv64.h b/compiler/utils/riscv64/assembler_riscv64.h index 4f561dc1a6..667d09dc74 100644 --- a/compiler/utils/riscv64/assembler_riscv64.h +++ b/compiler/utils/riscv64/assembler_riscv64.h @@ -1943,7 +1943,12 @@ class Riscv64Assembler final : public Assembler { class Branch { public: enum Type : uint8_t { - // TODO(riscv64): Support 16-bit instructions ("C" Standard Extension). + // Compressed branches (can be promoted to longer) + kCondCBranch, + kUncondCBranch, + // Compressed branches (can't be promoted to longer) + kBareCondCBranch, + kBareUncondCBranch, // Short branches (can be promoted to longer). kCondBranch, @@ -1954,7 +1959,9 @@ class Riscv64Assembler final : public Assembler { kBareUncondBranch, kBareCall, - // Medium branch (can be promoted to long). + // Medium branches (can be promoted to long). + // Compressed version + kCondCBranch21, kCondBranch21, // Long branches. @@ -1975,6 +1982,8 @@ class Riscv64Assembler final : public Assembler { // Bit sizes of offsets defined as enums to minimize chance of typos. enum OffsetBits { + kOffset9 = 9, + kOffset12 = 12, kOffset13 = 13, kOffset21 = 21, kOffset32 = 32, @@ -1996,14 +2005,16 @@ class Riscv64Assembler final : public Assembler { static const BranchInfo branch_info_[/* Type */]; // Unconditional branch or call. - Branch(uint32_t location, uint32_t target, XRegister rd, bool is_bare); + Branch( + uint32_t location, uint32_t target, XRegister rd, bool is_bare, bool compression_allowed); // Conditional branch. Branch(uint32_t location, uint32_t target, BranchCondition condition, XRegister lhs_reg, XRegister rhs_reg, - bool is_bare); + bool is_bare, + bool compression_allowed); // Label address or literal. Branch(uint32_t location, uint32_t target, XRegister rd, Type label_or_literal_type); Branch(uint32_t location, uint32_t target, FRegister rd, Type literal_type); @@ -2012,13 +2023,16 @@ class Riscv64Assembler final : public Assembler { // others are effectively unconditional. static bool IsNop(BranchCondition condition, XRegister lhs, XRegister rhs); static bool IsUncond(BranchCondition condition, XRegister lhs, XRegister rhs); + static bool IsCompressed(Type type); static BranchCondition OppositeCondition(BranchCondition cond); Type GetType() const; + Type GetOldType() const; BranchCondition GetCondition() const; XRegister GetLeftRegister() const; XRegister GetRightRegister() const; + XRegister GetNonZeroRegister() const; FRegister GetFRegister() const; uint32_t GetTarget() const; uint32_t GetLocation() const; @@ -2032,6 +2046,9 @@ class Riscv64Assembler final : public Assembler { uint32_t NextBranchId() const; + // Checks if condition meets compression requirements + bool IsCompressableCondition() const; + // Returns the bit size of the signed offset that the branch instruction can handle. OffsetBits GetOffsetSize() const; @@ -2067,6 +2084,11 @@ class Riscv64Assembler final : public Assembler { void InitializeType(Type initial_type); // Helper for the above. void InitShortOrLong(OffsetBits ofs_size, Type short_type, Type long_type, Type longest_type); + void InitShortOrLong(OffsetBits ofs_size, + Type compressed_type, + Type short_type, + Type long_type, + Type longest_type); uint32_t old_location_; // Offset into assembler buffer in bytes. uint32_t location_; // Offset into assembler buffer in bytes. @@ -2085,6 +2107,8 @@ class Riscv64Assembler final : public Assembler { // NOTE: encoded the same way as a position in a linked Label (id + sizeof(void*)) // Label itself is used to hold the 'head' of this list uint32_t next_branch_id_; + + bool compression_allowed_; }; // Branch and literal fixup. diff --git a/compiler/utils/riscv64/assembler_riscv64_test.cc b/compiler/utils/riscv64/assembler_riscv64_test.cc index c122706208..256ca12498 100644 --- a/compiler/utils/riscv64/assembler_riscv64_test.cc +++ b/compiler/utils/riscv64/assembler_riscv64_test.cc @@ -337,9 +337,11 @@ class AssemblerRISCV64Test : public AssemblerTest<Riscv64Assembler, } std::string EmitNops(size_t size) { - // TODO(riscv64): Support "C" Standard Extension. - DCHECK_ALIGNED(size, sizeof(uint32_t)); - const size_t num_nops = size / sizeof(uint32_t); + const size_t nop_size = GetAssembler()->IsExtensionEnabled(Riscv64Extension::kZca) ? + sizeof(uint16_t) : + sizeof(uint32_t); + DCHECK(IsAlignedParam(size, nop_size)); + const size_t num_nops = size / nop_size; return RepeatInsn(num_nops, "nop\n", [&]() { __ Nop(); }); } @@ -475,6 +477,25 @@ class AssemblerRISCV64Test : public AssemblerTest<Riscv64Assembler, }; } + auto GetPrintBcondOpposite() { + return [=]([[maybe_unused]] const std::string& cond, + const std::string& opposite_cond, + const std::string& args, + const std::string& target) { + return "b" + opposite_cond + args + ", " + target + "\n"; + }; + } + + auto GetPrintBcondAndJ(const std::string& skip_label) { + return [=](const std::string& cond, + [[maybe_unused]] const std::string& opposite_cond, + const std::string& args, + const std::string& target) { + return "b" + cond + args + ", " + skip_label + "f\n" + "j " + target + "\n" + skip_label + + ":\n"; + }; + } + auto GetPrintBcondOppositeAndJ(const std::string& skip_label) { return [=]([[maybe_unused]] const std::string& cond, const std::string& opposite_cond, @@ -579,34 +600,34 @@ class AssemblerRISCV64Test : public AssemblerTest<Riscv64Assembler, DriverStr(expected, test_name); } - size_t MaxOffset13BackwardDistance() { - return 4 * KB; - } + size_t MaxOffset9BackwardDistance() const { return KB / 4; } + size_t MaxOffset9ForwardDistance() const { return KB / 4 - 2; } - size_t MaxOffset13ForwardDistance() { - // TODO(riscv64): Support "C" Standard Extension, max forward distance 4KiB - 2. - return 4 * KB - 4; - } + size_t MaxOffset13BackwardDistance() const { return 4 * KB; } + size_t MaxOffset13ForwardDistance() const { return 4 * KB - 2; } - size_t MaxOffset21BackwardDistance() { - return 1 * MB; - } + size_t MaxOffset13BackwardDistance_WithoutC() const { return 4 * KB; } + size_t MaxOffset13ForwardDistance_WithoutC() const { return 4 * KB - 4; } - size_t MaxOffset21ForwardDistance() { - // TODO(riscv64): Support "C" Standard Extension, max forward distance 1MiB - 2. - return 1 * MB - 4; - } + size_t MaxOffset21BackwardDistance() const { return 1 * MB; } + size_t MaxOffset21ForwardDistance() const { return 1 * MB - 2; } + + size_t MaxOffset21BackwardDistance_WithoutC() { return 1 * MB; } + size_t MaxOffset21ForwardDistance_WithoutC() { return 1 * MB - 4; } template <typename PrintBcond> - void TestBeqA0A1Forward(const std::string& test_name, - size_t nops_size, - const std::string& target_label, - PrintBcond&& print_bcond, - bool is_bare = false) { + void TestBcondA0RegForward(const std::string& test_name, + void (Riscv64Assembler::*f)(XRegister, XRegister, Riscv64Label*, bool), + size_t nops_size, + const std::string& target_label, + PrintBcond&& print_bcond, + XRegister reg, + bool is_bare = false) { std::string expected; Riscv64Label label; - __ Beq(A0, A1, &label, is_bare); - expected += print_bcond("eq", "ne", " a0, a1", target_label + "f"); + (GetAssembler()->*f)(A0, reg, &label, is_bare); + std::string args = " a0, " + GetRegisterName(reg); + expected += print_bcond("eq", "ne", args, target_label + "f"); expected += EmitNops(nops_size); __ Bind(&label); expected += target_label + ":\n"; @@ -614,18 +635,22 @@ class AssemblerRISCV64Test : public AssemblerTest<Riscv64Assembler, } template <typename PrintBcond> - void TestBeqA0A1Backward(const std::string& test_name, - size_t nops_size, - const std::string& target_label, - PrintBcond&& print_bcond, - bool is_bare = false) { + void TestBcondA0RegBackward( + const std::string& test_name, + void (Riscv64Assembler::*f)(XRegister, XRegister, Riscv64Label*, bool), + size_t nops_size, + const std::string& target_label, + PrintBcond&& print_bcond, + XRegister reg, + bool is_bare = false) { std::string expected; Riscv64Label label; __ Bind(&label); expected += target_label + ":\n"; expected += EmitNops(nops_size); - __ Beq(A0, A1, &label, is_bare); - expected += print_bcond("eq", "ne", " a0, a1", target_label + "b"); + (GetAssembler()->*f)(A0, reg, &label, is_bare); + std::string args = " a0, " + GetRegisterName(reg); + expected += print_bcond("eq", "ne", args, target_label + "b"); DriverStr(expected, test_name); } @@ -8399,11 +8424,16 @@ TEST_F(AssemblerRISCV64Test, Jalr0_WithoutC) { } TEST_F(AssemblerRISCV64Test, Ret) { - ScopedCSuppression scs(this); __ Ret(); DriverStr("ret\n", "Ret"); } +TEST_F(AssemblerRISCV64Test, Ret_WithoutC) { + ScopedCSuppression scs(this); + __ Ret(); + DriverStr("ret\n", "Ret_WithoutC"); +} + TEST_F(AssemblerRISCV64Test, RdCycle) { DriverStr(RepeatR(&Riscv64Assembler::RdCycle, "rdcycle {reg}\n"), "RdCycle"); } @@ -8523,100 +8553,263 @@ TEST_F(AssemblerRISCV64Test, BcondBackward2MiB) { TestBcondBackward("BcondBackward2MiB", 2 * MB, "1", GetPrintBcondOppositeAndTail("2", "3")); } +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset9Forward) { + TestBcondA0RegForward("BeqA0ZeroMaxOffset9Forward", + &Riscv64Assembler::Beq, + MaxOffset9ForwardDistance() - /*C.BEQZ*/ 2u, + "1", + GetPrintBcond(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset13Forward) { + TestBcondA0RegForward("BeqA0ZeroMaxOffset13Forward", + &Riscv64Assembler::Beq, + MaxOffset13ForwardDistance() - /*C.BEQZ*/ 2u, + "1", + GetPrintBcond(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BneA0ZeroMaxOffset9ForwardOpposite) { + TestBcondA0RegForward("BeqA0ZeroMaxOffset9ForwardOpposite", + &Riscv64Assembler::Bne, + MaxOffset9ForwardDistance() - /*C.BNEZ*/ 2u, + "1", + GetPrintBcondOpposite(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BneA0ZeroMaxOffset13Forward) { + TestBcondA0RegForward("BneA0ZeroMaxOffset13Forward", + &Riscv64Assembler::Bne, + MaxOffset13ForwardDistance() - /*C.BbeZ*/ 2u, + "1", + GetPrintBcondOpposite(), + Zero); +} + TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset13Forward) { ScopedCSuppression scs(this); - TestBeqA0A1Forward("BeqA0A1MaxOffset13Forward", - MaxOffset13ForwardDistance() - /*BEQ*/ 4u, - "1", - GetPrintBcond()); + TestBcondA0RegForward("BeqA0A1MaxOffset13Forward", + &Riscv64Assembler::Beq, + MaxOffset13ForwardDistance_WithoutC() - /*BEQ*/ 4u, + "1", + GetPrintBcond(), + A1); } TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset13ForwardBare) { ScopedCSuppression scs(this); - TestBeqA0A1Forward("BeqA0A1MaxOffset13ForwardBare", - MaxOffset13ForwardDistance() - /*BEQ*/ 4u, - "1", - GetPrintBcond(), - /*is_bare=*/ true); + TestBcondA0RegForward("BeqA0A1MaxOffset13ForwardBare", + &Riscv64Assembler::Beq, + MaxOffset13ForwardDistance_WithoutC() - /*BEQ*/ 4u, + "1", + GetPrintBcond(), + A1, + /*is_bare=*/true); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset9Backward) { + TestBcondA0RegBackward("BeqA0ZeroMaxOffset9Backward", + &Riscv64Assembler::Beq, + MaxOffset9BackwardDistance() - /*C.BEQZ*/ 2u, + "1", + GetPrintBcond(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset13Backward) { + TestBcondA0RegBackward("BeqA0ZeroMaxOffset13Backward", + &Riscv64Assembler::Beq, + MaxOffset13ForwardDistance() - /*BEQ*/ 4u, + "1", + GetPrintBcond(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset9BackwardOpposite) { + TestBcondA0RegBackward("BeqA0ZeroMaxOffset9BackwardOpposite", + &Riscv64Assembler::Bne, + MaxOffset9BackwardDistance() - /*C.BNEZ*/ 2u, + "1", + GetPrintBcondOpposite(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset13BackwardOpposite) { + TestBcondA0RegBackward("BeqA0ZeroMaxOffset13BackwardOpposite", + &Riscv64Assembler::Bne, + MaxOffset13ForwardDistance() - /*BNE*/ 4u, + "1", + GetPrintBcondOpposite(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset13Backward_WithoutC) { + ScopedCSuppression scs(this); + TestBcondA0RegBackward("BeqA0A1MaxOffset13Forward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset13BackwardDistance_WithoutC(), + "1", + GetPrintBcond(), + A1); } -TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset13Backward) { +TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset13BackwardBare_WithoutC) { ScopedCSuppression scs(this); - TestBeqA0A1Backward("BeqA0A1MaxOffset13Forward", - MaxOffset13BackwardDistance(), - "1", - GetPrintBcond()); + TestBcondA0RegBackward("BeqA0A1MaxOffset13ForwardBare_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset13BackwardDistance_WithoutC(), + "1", + GetPrintBcond(), + A1, + /*is_bare=*/true); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroOverMaxOffset9Forward) { + TestBcondA0RegForward("BeqA0ZeroOverMaxOffset9Forward", + &Riscv64Assembler::Beq, + MaxOffset9ForwardDistance() - /*C.BEQZ*/ 2u + /*Exceed max*/ 2u, + "1", + GetPrintBcond(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroOverMaxOffset9Backward) { + TestBcondA0RegBackward("BeqA0ZeroOverMaxOffset9Backward", + &Riscv64Assembler::Beq, + MaxOffset9BackwardDistance() - /*C.BEQZ*/ 2u + /*Exceed max*/ 2u, + "1", + GetPrintBcond(), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset13Forward_WithoutC) { + ScopedCSuppression scs(this); + TestBcondA0RegForward("BeqA0A1OverMaxOffset13Forward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset13ForwardDistance_WithoutC() - /*BEQ*/ 4u + /*Exceed max*/ 4u, + "1", + GetPrintBcondOppositeAndJ("2"), + A1); } -TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset13BackwardBare) { +TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset13Backward_WithoutC) { ScopedCSuppression scs(this); - TestBeqA0A1Backward("BeqA0A1MaxOffset13ForwardBare", - MaxOffset13BackwardDistance(), - "1", - GetPrintBcond(), - /*is_bare=*/ true); + TestBcondA0RegBackward("BeqA0A1OverMaxOffset13Forward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset13BackwardDistance_WithoutC() + /*Exceed max*/ 4u, + "1", + GetPrintBcondOppositeAndJ("2"), + A1); } -TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset13Forward) { +TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset21Forward_WithoutC) { ScopedCSuppression scs(this); - TestBeqA0A1Forward("BeqA0A1OverMaxOffset13Forward", - MaxOffset13ForwardDistance() - /*BEQ*/ 4u + /*Exceed max*/ 4u, - "1", - GetPrintBcondOppositeAndJ("2")); + TestBcondA0RegForward("BeqA0A1MaxOffset21Forward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u, + "1", + GetPrintBcondOppositeAndJ("2"), + A1); } -TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset13Backward) { +TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset21Backward_WithoutC) { ScopedCSuppression scs(this); - TestBeqA0A1Backward("BeqA0A1OverMaxOffset13Forward", - MaxOffset13BackwardDistance() + /*Exceed max*/ 4u, - "1", - GetPrintBcondOppositeAndJ("2")); + TestBcondA0RegBackward("BeqA0A1MaxOffset21Backward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset21BackwardDistance_WithoutC() - /*BNE*/ 4u, + "1", + GetPrintBcondOppositeAndJ("2"), + A1); } -TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset21Forward) { - ScopedCSuppression scs(this); - TestBeqA0A1Forward("BeqA0A1MaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u, - "1", - GetPrintBcondOppositeAndJ("2")); +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset21Backward) { + TestBcondA0RegBackward("BeqA0ZeroMaxOffset21Backward", + &Riscv64Assembler::Beq, + MaxOffset21BackwardDistance() - /*BNE*/ 4u, + "1", + GetPrintBcondOppositeAndJ("2"), + Zero); } -TEST_F(AssemblerRISCV64Test, BeqA0A1MaxOffset21Backward) { - ScopedCSuppression scs(this); - TestBeqA0A1Backward("BeqA0A1MaxOffset21Backward", - MaxOffset21BackwardDistance() - /*BNE*/ 4u, - "1", - GetPrintBcondOppositeAndJ("2")); +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset21BackwardNe) { + TestBcondA0RegBackward("BeqA0ZeroMaxOffset21BackwardNe", + &Riscv64Assembler::Bne, + MaxOffset21BackwardDistance() - /*BNE*/ 4u, + "1", + GetPrintBcondAndJ("2"), + Zero); } -TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset21Forward) { +TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset21Forward_WithoutC) { ScopedCSuppression scs(this); - TestBeqA0A1Forward("BeqA0A1OverMaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u + /*Exceed max*/ 4u, - "1", - GetPrintBcondOppositeAndTail("2", "3")); + TestBcondA0RegForward("BeqA0A1OverMaxOffset21Forward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u + /*Exceed max*/ 4u, + "1", + GetPrintBcondOppositeAndTail("2", "3"), + A1); } -TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset21Backward) { +TEST_F(AssemblerRISCV64Test, BeqA0A1OverMaxOffset21Backward_WithoutC) { ScopedCSuppression scs(this); - TestBeqA0A1Backward("BeqA0A1OverMaxOffset21Backward", - MaxOffset21BackwardDistance() - /*BNE*/ 4u + /*Exceed max*/ 4u, - "1", - GetPrintBcondOppositeAndTail("2", "3")); + TestBcondA0RegBackward("BeqA0A1OverMaxOffset21Backward_WithoutC", + &Riscv64Assembler::Beq, + MaxOffset21BackwardDistance_WithoutC() - /*BNE*/ 4u + /*Exceed max*/ 4u, + "1", + GetPrintBcondOppositeAndTail("2", "3"), + A1); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset21Forward) { + TestBcondA0RegForward("BeqA0ZeroMaxOffset21Forward", + &Riscv64Assembler::Beq, + MaxOffset21ForwardDistance() - /*J*/ 4u, + "1", + GetPrintBcondOppositeAndJ("2"), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset21ForwardNe) { + TestBcondA0RegForward("BeqA0ZeroMaxOffset21ForwardNe", + &Riscv64Assembler::Bne, + MaxOffset21ForwardDistance() - /*J*/ 4u, + "1", + GetPrintBcondAndJ("2"), + Zero); +} + +TEST_F(AssemblerRISCV64Test, BeqA0ZeroMaxOffset21ForwardNeNotCompressable) { + TestBcondA0RegForward("BeqA0ZeroMaxOffset21ForwardNeNotCompressable", + &Riscv64Assembler::Bne, + MaxOffset21ForwardDistance() - /*BNE*/ 50u, + "1", + GetPrintBcondAndJ("2"), + A2); +} + +TEST_F(AssemblerRISCV64Test, BeqA0A1AlmostCascade_WithoutC) { + ScopedCSuppression scs(this); + TestBeqA0A1MaybeCascade("BeqA0A1AlmostCascade_WithoutC", /*cascade=*/false, GetPrintBcond()); } TEST_F(AssemblerRISCV64Test, BeqA0A1AlmostCascade) { - ScopedCSuppression scs(this); - TestBeqA0A1MaybeCascade("BeqA0A1AlmostCascade", /*cascade=*/ false, GetPrintBcond()); + TestBeqA0A1MaybeCascade("BeqA0A1AlmostCascade", /*cascade=*/false, GetPrintBcond()); } -TEST_F(AssemblerRISCV64Test, BeqA0A1Cascade) { +TEST_F(AssemblerRISCV64Test, BeqA0A1Cascade_WithoutC) { ScopedCSuppression scs(this); TestBeqA0A1MaybeCascade( - "BeqA0A1AlmostCascade", /*cascade=*/ true, GetPrintBcondOppositeAndJ("1")); + "BeqA0A1AlmostCascade_WithoutC", /*cascade=*/true, GetPrintBcondOppositeAndJ("1")); } -TEST_F(AssemblerRISCV64Test, BcondElimination) { +TEST_F(AssemblerRISCV64Test, BeqA0A1Cascade) { + TestBeqA0A1MaybeCascade("BeqA0A1AlmostCascade", /*cascade=*/true, GetPrintBcondOppositeAndJ("1")); +} + +TEST_F(AssemblerRISCV64Test, BcondElimination_WithoutC) { ScopedCSuppression scs(this); Riscv64Label label; __ Bind(&label); @@ -8628,10 +8821,10 @@ TEST_F(AssemblerRISCV64Test, BcondElimination) { __ Bltu(reg, reg, &label); __ Bgtu(reg, reg, &label); } - DriverStr("nop\n", "BcondElimination"); + DriverStr("nop\n", "BcondElimination_WithoutC"); } -TEST_F(AssemblerRISCV64Test, BcondUnconditional) { +TEST_F(AssemblerRISCV64Test, BcondUnconditional_WithoutC) { ScopedCSuppression scs(this); Riscv64Label label; __ Bind(&label); @@ -8647,174 +8840,199 @@ TEST_F(AssemblerRISCV64Test, BcondUnconditional) { "1:\n" "nop\n" + RepeatInsn(5u * GetRegisters().size(), "j 1b\n", []() {}); - DriverStr(expected, "BcondUnconditional"); + DriverStr(expected, "BcondUnconditional_WithoutC"); } -TEST_F(AssemblerRISCV64Test, JalRdForward3KiB) { +TEST_F(AssemblerRISCV64Test, JalRdForward3KiB_WithoutC) { ScopedCSuppression scs(this); - TestJalRdForward("JalRdForward3KiB", 3 * KB, "1", GetPrintJalRd()); + TestJalRdForward("JalRdForward3KiB_WithoutC", 3 * KB, "1", GetPrintJalRd()); } -TEST_F(AssemblerRISCV64Test, JalRdForward3KiBBare) { +TEST_F(AssemblerRISCV64Test, JalRdForward3KiBBare_WithoutC) { ScopedCSuppression scs(this); - TestJalRdForward("JalRdForward3KiB", 3 * KB, "1", GetPrintJalRd(), /*is_bare=*/ true); + TestJalRdForward("JalRdForward3KiB_WithoutC", 3 * KB, "1", GetPrintJalRd(), /*is_bare=*/true); } -TEST_F(AssemblerRISCV64Test, JalRdBackward3KiB) { +TEST_F(AssemblerRISCV64Test, JalRdBackward3KiB_WithoutC) { ScopedCSuppression scs(this); - TestJalRdBackward("JalRdBackward3KiB", 3 * KB, "1", GetPrintJalRd()); + TestJalRdBackward("JalRdBackward3KiB_WithoutC", 3 * KB, "1", GetPrintJalRd()); } -TEST_F(AssemblerRISCV64Test, JalRdBackward3KiBBare) { +TEST_F(AssemblerRISCV64Test, JalRdBackward3KiBBare_WithoutC) { ScopedCSuppression scs(this); - TestJalRdBackward("JalRdBackward3KiB", 3 * KB, "1", GetPrintJalRd(), /*is_bare=*/ true); + TestJalRdBackward("JalRdBackward3KiB_WithoutC", 3 * KB, "1", GetPrintJalRd(), /*is_bare=*/true); } -TEST_F(AssemblerRISCV64Test, JalRdForward2MiB) { +TEST_F(AssemblerRISCV64Test, JalRdForward2MiB_WithoutC) { ScopedCSuppression scs(this); - TestJalRdForward("JalRdForward2MiB", 2 * MB, "1", GetPrintCallRd("2")); + TestJalRdForward("JalRdForward2MiB_WithoutC", 2 * MB, "1", GetPrintCallRd("2")); } -TEST_F(AssemblerRISCV64Test, JalRdBackward2MiB) { +TEST_F(AssemblerRISCV64Test, JalRdBackward2MiB_WithoutC) { ScopedCSuppression scs(this); - TestJalRdBackward("JalRdBackward2MiB", 2 * MB, "1", GetPrintCallRd("2")); + TestJalRdBackward("JalRdBackward2MiB_WithoutC", 2 * MB, "1", GetPrintCallRd("2")); } -TEST_F(AssemblerRISCV64Test, JForward3KiB) { +TEST_F(AssemblerRISCV64Test, JForward3KiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("JForward3KiB", 3 * KB, "1", GetEmitJ(), GetPrintJ()); + TestBuncondForward("JForward3KiB_WithoutC", 3 * KB, "1", GetEmitJ(), GetPrintJ()); +} + +TEST_F(AssemblerRISCV64Test, JForward3KiBWithCompression) { + TestBuncondForward("JForward3KiBWithCompression", 3 * KB, "1", GetEmitJ(), GetPrintJ()); +} + +TEST_F(AssemblerRISCV64Test, JForward2KiB) { + TestBuncondForward("JForward2KiB", 2 * KB - 4, "1", GetEmitJ(), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JForward3KiBBare) { +TEST_F(AssemblerRISCV64Test, JForward3KiBBare_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("JForward3KiB", 3 * KB, "1", GetEmitJ(/*is_bare=*/ true), GetPrintJ()); + TestBuncondForward("JForward3KiB_WithoutC", 3 * KB, "1", GetEmitJ(/*is_bare=*/true), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JBackward3KiB) { +TEST_F(AssemblerRISCV64Test, JForward2KiBBare) { + TestBuncondForward("JForward2KiBBare", 2 * KB - 4, "1", GetEmitJ(/*is_bare=*/true), GetPrintJ()); +} + +TEST_F(AssemblerRISCV64Test, JBackward3KiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("JBackward3KiB", 3 * KB, "1", GetEmitJ(), GetPrintJ()); + TestBuncondBackward("JBackward3KiB_WithoutC", 3 * KB, "1", GetEmitJ(), GetPrintJ()); +} + +TEST_F(AssemblerRISCV64Test, JBackward3KiBWithCompression) { + TestBuncondBackward("JBackward3KiBWithCompression", 3 * KB, "1", GetEmitJ(), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JBackward3KiBBare) { +TEST_F(AssemblerRISCV64Test, JBackward2KiB) { + TestBuncondBackward("JBackward2KiB", 2 * KB, "1", GetEmitJ(), GetPrintJ()); +} + +TEST_F(AssemblerRISCV64Test, JBackward3KiBBare_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("JBackward3KiB", 3 * KB, "1", GetEmitJ(/*is_bare=*/ true), GetPrintJ()); + TestBuncondBackward( + "JBackward3KiB_WithoutC", 3 * KB, "1", GetEmitJ(/*is_bare=*/true), GetPrintJ()); +} + +TEST_F(AssemblerRISCV64Test, JBackward2KiBBare) { + TestBuncondBackward("JBackward2KiBBare", 2 * KB, "1", GetEmitJ(/*is_bare=*/true), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JForward2MiB) { +TEST_F(AssemblerRISCV64Test, JForward2MiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("JForward2MiB", 2 * MB, "1", GetEmitJ(), GetPrintTail("2")); + TestBuncondForward("JForward2MiB_WithoutC", 2 * MB, "1", GetEmitJ(), GetPrintTail("2")); } -TEST_F(AssemblerRISCV64Test, JBackward2MiB) { +TEST_F(AssemblerRISCV64Test, JBackward2MiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("JBackward2MiB", 2 * MB, "1", GetEmitJ(), GetPrintTail("2")); + TestBuncondBackward("JBackward2MiB_WithoutC", 2 * MB, "1", GetEmitJ(), GetPrintTail("2")); } -TEST_F(AssemblerRISCV64Test, JMaxOffset21Forward) { +TEST_F(AssemblerRISCV64Test, JMaxOffset21Forward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("JMaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u, + TestBuncondForward("JMaxOffset21Forward_WithoutC", + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u, "1", GetEmitJ(), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JMaxOffset21ForwardBare) { +TEST_F(AssemblerRISCV64Test, JMaxOffset21ForwardBare_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("JMaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u, + TestBuncondForward("JMaxOffset21Forward_WithoutC", + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u, "1", - GetEmitJ(/*is_bare=*/ true), + GetEmitJ(/*is_bare=*/true), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JMaxOffset21Backward) { +TEST_F(AssemblerRISCV64Test, JMaxOffset21Backward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("JMaxOffset21Backward", - MaxOffset21BackwardDistance(), + TestBuncondBackward("JMaxOffset21Backward_WithoutC", + MaxOffset21BackwardDistance_WithoutC(), "1", GetEmitJ(), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JMaxOffset21BackwardBare) { +TEST_F(AssemblerRISCV64Test, JMaxOffset21BackwardBare_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("JMaxOffset21Backward", - MaxOffset21BackwardDistance(), + TestBuncondBackward("JMaxOffset21Backward_WithoutC", + MaxOffset21BackwardDistance_WithoutC(), "1", - GetEmitJ(/*is_bare=*/ true), + GetEmitJ(/*is_bare=*/true), GetPrintJ()); } -TEST_F(AssemblerRISCV64Test, JOverMaxOffset21Forward) { +TEST_F(AssemblerRISCV64Test, JOverMaxOffset21Forward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("JOverMaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u + /*Exceed max*/ 4u, + TestBuncondForward("JOverMaxOffset21Forward_WithoutC", + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u + /*Exceed max*/ 4u, "1", GetEmitJ(), GetPrintTail("2")); } -TEST_F(AssemblerRISCV64Test, JOverMaxOffset21Backward) { +TEST_F(AssemblerRISCV64Test, JOverMaxOffset21Backward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("JMaxOffset21Backward", - MaxOffset21BackwardDistance() + /*Exceed max*/ 4u, + TestBuncondBackward("JMaxOffset21Backward_WithoutC", + MaxOffset21BackwardDistance_WithoutC() + /*Exceed max*/ 4u, "1", GetEmitJ(), GetPrintTail("2")); } -TEST_F(AssemblerRISCV64Test, CallForward3KiB) { +TEST_F(AssemblerRISCV64Test, CallForward3KiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("CallForward3KiB", 3 * KB, "1", GetEmitJal(), GetPrintJal()); + TestBuncondForward("CallForward3KiB_WithoutC", 3 * KB, "1", GetEmitJal(), GetPrintJal()); } -TEST_F(AssemblerRISCV64Test, CallBackward3KiB) { +TEST_F(AssemblerRISCV64Test, CallBackward3KiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("CallBackward3KiB", 3 * KB, "1", GetEmitJal(), GetPrintJal()); + TestBuncondBackward("CallBackward3KiB_WithoutC", 3 * KB, "1", GetEmitJal(), GetPrintJal()); } -TEST_F(AssemblerRISCV64Test, CallForward2MiB) { +TEST_F(AssemblerRISCV64Test, CallForward2MiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("CallForward2MiB", 2 * MB, "1", GetEmitJal(), GetPrintCall("2")); + TestBuncondForward("CallForward2MiB_WithoutC", 2 * MB, "1", GetEmitJal(), GetPrintCall("2")); } -TEST_F(AssemblerRISCV64Test, CallBackward2MiB) { +TEST_F(AssemblerRISCV64Test, CallBackward2MiB_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("CallBackward2MiB", 2 * MB, "1", GetEmitJal(), GetPrintCall("2")); + TestBuncondBackward("CallBackward2MiB_WithoutC", 2 * MB, "1", GetEmitJal(), GetPrintCall("2")); } -TEST_F(AssemblerRISCV64Test, CallMaxOffset21Forward) { +TEST_F(AssemblerRISCV64Test, CallMaxOffset21Forward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("CallMaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u, + TestBuncondForward("CallMaxOffset21Forward_WithoutC", + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u, "1", GetEmitJal(), GetPrintJal()); } -TEST_F(AssemblerRISCV64Test, CallMaxOffset21Backward) { +TEST_F(AssemblerRISCV64Test, CallMaxOffset21Backward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("CallMaxOffset21Backward", - MaxOffset21BackwardDistance(), + TestBuncondBackward("CallMaxOffset21Backward_WithoutC", + MaxOffset21BackwardDistance_WithoutC(), "1", GetEmitJal(), GetPrintJal()); } -TEST_F(AssemblerRISCV64Test, CallOverMaxOffset21Forward) { +TEST_F(AssemblerRISCV64Test, CallOverMaxOffset21Forward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondForward("CallOverMaxOffset21Forward", - MaxOffset21ForwardDistance() - /*J*/ 4u + /*Exceed max*/ 4u, + TestBuncondForward("CallOverMaxOffset21Forward_WithoutC", + MaxOffset21ForwardDistance_WithoutC() - /*J*/ 4u + /*Exceed max*/ 4u, "1", GetEmitJal(), GetPrintCall("2")); } -TEST_F(AssemblerRISCV64Test, CallOverMaxOffset21Backward) { +TEST_F(AssemblerRISCV64Test, CallOverMaxOffset21Backward_WithoutC) { ScopedCSuppression scs(this); - TestBuncondBackward("CallMaxOffset21Backward", - MaxOffset21BackwardDistance() + /*Exceed max*/ 4u, + TestBuncondBackward("CallMaxOffset21Backward_WithoutC", + MaxOffset21BackwardDistance_WithoutC() + /*Exceed max*/ 4u, "1", GetEmitJal(), GetPrintCall("2")); diff --git a/dex2oat/Android.bp b/dex2oat/Android.bp index 4002cb55ee..1bec1cf889 100644 --- a/dex2oat/Android.bp +++ b/dex2oat/Android.bp @@ -77,11 +77,13 @@ art_cc_defaults { generated_sources: ["art_dex2oat_operator_srcs"], shared_libs: [ "libbase", - "libcrypto", // For SHA-1 checksumming of build ID "liblog", "liblz4", "libz", ], + static_libs: [ + "libcrypto_for_art", // For SHA-1 checksumming of build ID + ], export_include_dirs: ["."], } @@ -89,7 +91,7 @@ cc_defaults { name: "libart-dex2oat_static_base_defaults", whole_static_libs: [ "libbase", - "libcrypto", + "libcrypto_for_art", "liblog", "liblz4", "libz", @@ -221,12 +223,14 @@ cc_defaults { shared_libs: [ "libartpalette", "libbase", - "libcrypto", "liblz4", // libart(d)-dex2oat dependency; must be repeated here since it's a static lib. "liblog", "libsigchain", "libz", ], + static_libs: [ + "libcrypto_for_art", + ], }, }, } @@ -500,7 +504,7 @@ art_cc_defaults { "liblog", ], static_libs: [ - "libcrypto_static", // Must be statically linked in standalone tests + "libcrypto_for_art", "liblz4", // libart(d)-dex2oat dependency; must be repeated here since it's a static lib. ], } diff --git a/dex2oat/driver/compiler_driver.cc b/dex2oat/driver/compiler_driver.cc index 6423cc3ead..c696191491 100644 --- a/dex2oat/driver/compiler_driver.cc +++ b/dex2oat/driver/compiler_driver.cc @@ -2229,7 +2229,7 @@ class InitializeClassVisitor : public CompilationVisitor { const dex::TypeId& class_type_id = dex_file.GetTypeId(class_def->class_idx_); const char* descriptor = dex_file.GetStringData(class_type_id.descriptor_idx_); StackHandleScope<3> hs(self); - ClassLinker* const class_linker = manager_->GetClassLinker(); + AotClassLinker* const class_linker = down_cast<AotClassLinker*>(manager_->GetClassLinker()); Runtime* const runtime = Runtime::Current(); const CompilerOptions& compiler_options = manager_->GetCompiler()->GetCompilerOptions(); const bool is_boot_image = compiler_options.IsBootImage(); @@ -2338,7 +2338,7 @@ class InitializeClassVisitor : public CompilationVisitor { DCHECK(exception_initialized); // Run the class initializer in transaction mode. - runtime->EnterTransactionMode(is_app_image, klass.Get()); + class_linker->EnterTransactionMode(is_app_image, klass.Get()); bool success = class_linker->EnsureInitialized(self, klass, true, true); // TODO we detach transaction from runtime to indicate we quit the transactional @@ -2349,7 +2349,7 @@ class InitializeClassVisitor : public CompilationVisitor { ScopedAssertNoThreadSuspension ants("Transaction end"); if (success) { - runtime->ExitTransactionMode(); + class_linker->ExitTransactionMode(); DCHECK(!runtime->IsActiveTransaction()); if (is_boot_image || is_boot_image_extension) { @@ -2370,7 +2370,7 @@ class InitializeClassVisitor : public CompilationVisitor { *file_log << exception->Dump() << "\n"; } self->ClearException(); - runtime->RollbackAllTransactions(); + class_linker->RollbackAllTransactions(); CHECK_EQ(old_status, klass->GetStatus()) << "Previous class status not restored"; } } @@ -2419,9 +2419,9 @@ class InitializeClassVisitor : public CompilationVisitor { self->GetJniEnv()->AssertLocalsEmpty(); } - if (!klass->IsVisiblyInitialized() && + if (!klass->IsInitialized() && (is_boot_image || is_boot_image_extension) && - !compiler_options.IsPreloadedClass(PrettyDescriptor(descriptor).c_str())) { + !compiler_options.IsPreloadedClass(PrettyDescriptor(descriptor))) { klass->SetInBootImageAndNotInPreloadedClasses(); } diff --git a/dex2oat/linker/image_writer.cc b/dex2oat/linker/image_writer.cc index abc3354421..9c83da5491 100644 --- a/dex2oat/linker/image_writer.cc +++ b/dex2oat/linker/image_writer.cc @@ -452,8 +452,6 @@ static void ClearDexFileCookies() REQUIRES_SHARED(Locks::mutator_lock_) { } bool ImageWriter::PrepareImageAddressSpace(TimingLogger* timings) { - target_ptr_size_ = InstructionSetPointerSize(compiler_options_.GetInstructionSet()); - Thread* const self = Thread::Current(); gc::Heap* const heap = Runtime::Current()->GetHeap(); @@ -3506,35 +3504,52 @@ const uint8_t* ImageWriter::GetQuickCode(ArtMethod* method, const ImageInfo& ima return quick_code; } +static inline uint32_t ResetNterpFastPathFlags( + uint32_t access_flags, ArtMethod* orig, InstructionSet isa) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(orig != nullptr); + DCHECK(!orig->IsProxyMethod()); // `UnstartedRuntime` does not support creating proxy classes. + DCHECK(!orig->IsRuntimeMethod()); + + // Clear old nterp fast path flags. + access_flags = ArtMethod::ClearNterpFastPathFlags(access_flags); + + // Check if nterp fast paths are available on the target ISA. + std::string_view shorty = orig->GetShortyView(); // Use orig, copy's class not yet ready. + uint32_t new_nterp_flags = GetNterpFastPathFlags(shorty, access_flags, isa); + + // Add the new nterp fast path flags, if any. + return access_flags | new_nterp_flags; +} + void ImageWriter::CopyAndFixupMethod(ArtMethod* orig, ArtMethod* copy, size_t oat_index) { - if (orig->IsAbstract()) { - // Ignore the single-implementation info for abstract method. - // Do this on orig instead of copy, otherwise there is a crash due to methods - // are copied before classes. - // TODO: handle fixup of single-implementation method for abstract method. - orig->SetHasSingleImplementation(false); - orig->SetSingleImplementation( - nullptr, Runtime::Current()->GetClassLinker()->GetImagePointerSize()); - } - - if (!orig->IsRuntimeMethod()) { - // If we're compiling a boot image and we have a profile, set methods as - // being shared memory (to avoid dirtying them with hotness counter). We - // expect important methods to be AOT, and non-important methods to be run - // in the interpreter. - if (CompilerFilter::DependsOnProfile(compiler_options_.GetCompilerFilter()) && - (compiler_options_.IsBootImage() || compiler_options_.IsBootImageExtension())) { - orig->SetMemorySharedMethod(); - } - } - memcpy(copy, orig, ArtMethod::Size(target_ptr_size_)); CopyAndFixupReference(copy->GetDeclaringClassAddressWithoutBarrier(), orig->GetDeclaringClassUnchecked<kWithoutReadBarrier>()); - ResetNterpFastPathFlags(copy, orig); + + if (!orig->IsRuntimeMethod()) { + uint32_t access_flags = orig->GetAccessFlags(); + if (ArtMethod::IsAbstract(access_flags)) { + // Ignore the single-implementation info for abstract method. + // TODO: handle fixup of single-implementation method for abstract method. + access_flags = ArtMethod::SetHasSingleImplementation(access_flags, /*single_impl=*/ false); + copy->SetSingleImplementation(nullptr, target_ptr_size_); + } else if (mark_memory_shared_methods_ && LIKELY(!ArtMethod::IsIntrinsic(access_flags))) { + access_flags = ArtMethod::SetMemorySharedMethod(access_flags); + copy->SetHotCounter(); + } + + InstructionSet isa = compiler_options_.GetInstructionSet(); + if (isa != kRuntimeISA) { + access_flags = ResetNterpFastPathFlags(access_flags, orig, isa); + } else { + DCHECK_EQ(access_flags, ResetNterpFastPathFlags(access_flags, orig, isa)); + } + copy->SetAccessFlags(access_flags); + } // OatWriter replaces the code_ with an offset value. Here we re-adjust to a pointer relative to // oat_begin_ @@ -3754,11 +3769,17 @@ ImageWriter::ImageWriter(const CompilerOptions& compiler_options, jobject class_loader, const std::vector<std::string>* dirty_image_objects) : compiler_options_(compiler_options), + target_ptr_size_(InstructionSetPointerSize(compiler_options.GetInstructionSet())), + // If we're compiling a boot image and we have a profile, set methods as being shared + // memory (to avoid dirtying them with hotness counter). We expect important methods + // to be AOT, and non-important methods to be run in the interpreter. + mark_memory_shared_methods_( + CompilerFilter::DependsOnProfile(compiler_options_.GetCompilerFilter()) && + (compiler_options_.IsBootImage() || compiler_options_.IsBootImageExtension())), boot_image_begin_(Runtime::Current()->GetHeap()->GetBootImagesStartAddress()), boot_image_size_(Runtime::Current()->GetHeap()->GetBootImagesSize()), global_image_begin_(reinterpret_cast<uint8_t*>(image_begin)), image_objects_offset_begin_(0), - target_ptr_size_(InstructionSetPointerSize(compiler_options.GetInstructionSet())), image_infos_(oat_filenames.size()), jni_stub_map_(JniStubKeyHash(compiler_options.GetInstructionSet()), JniStubKeyEquals(compiler_options.GetInstructionSet())), @@ -3841,32 +3862,5 @@ void ImageWriter::CopyAndFixupPointer(void* object, MemberOffset offset, ValueTy return CopyAndFixupPointer(object, offset, src_value, target_ptr_size_); } -void ImageWriter::ResetNterpFastPathFlags(ArtMethod* copy, ArtMethod* orig) { - DCHECK(copy != nullptr); - DCHECK(orig != nullptr); - if (orig->IsRuntimeMethod() || orig->IsProxyMethod()) { - return; // !IsRuntimeMethod() and !IsProxyMethod() for GetShortyView() - } - - // Clear old nterp fast path flags. - if (copy->HasNterpEntryPointFastPathFlag()) { - copy->ClearNterpEntryPointFastPathFlag(); // Flag has other uses, clear it conditionally. - } - copy->ClearNterpInvokeFastPathFlag(); - - // Check if nterp fast paths available on target ISA. - std::string_view shorty = orig->GetShortyView(); // Use orig, copy's class not yet ready. - uint32_t new_nterp_flags = - GetNterpFastPathFlags(shorty, copy->GetAccessFlags(), compiler_options_.GetInstructionSet()); - - // Set new nterp fast path flags, if approporiate. - if ((new_nterp_flags & kAccNterpEntryPointFastPathFlag) != 0) { - copy->SetNterpEntryPointFastPathFlag(); - } - if ((new_nterp_flags & kAccNterpInvokeFastPathFlag) != 0) { - copy->SetNterpInvokeFastPathFlag(); - } -} - } // namespace linker } // namespace art diff --git a/dex2oat/linker/image_writer.h b/dex2oat/linker/image_writer.h index 8ffe226bec..efe8fa2304 100644 --- a/dex2oat/linker/image_writer.h +++ b/dex2oat/linker/image_writer.h @@ -618,9 +618,6 @@ class ImageWriter final { void CopyAndFixupPointer(void* object, MemberOffset offset, ValueType src_value) REQUIRES_SHARED(Locks::mutator_lock_); - void ResetNterpFastPathFlags(ArtMethod* copy, ArtMethod* orig) - REQUIRES_SHARED(Locks::mutator_lock_); - ALWAYS_INLINE static bool IsStronglyInternedString(ObjPtr<mirror::String> str) REQUIRES_SHARED(Locks::mutator_lock_); @@ -639,6 +636,12 @@ class ImageWriter final { const CompilerOptions& compiler_options_; + // Size of pointers on the target architecture. + PointerSize target_ptr_size_; + + // Whether to mark non-abstract, non-intrinsic methods as "memory shared methods". + bool mark_memory_shared_methods_; + // Cached boot image begin and size. This includes heap, native objects and oat files. const uint32_t boot_image_begin_; const uint32_t boot_image_size_; @@ -656,9 +659,6 @@ class ImageWriter final { // Oat index map for objects. HashMap<mirror::Object*, uint32_t> oat_index_map_; - // Size of pointers on the target architecture. - PointerSize target_ptr_size_; - // Image data indexed by the oat file index. dchecked_vector<ImageInfo> image_infos_; diff --git a/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl b/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl index 0b50d22778..f8770147fc 100644 --- a/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl +++ b/dexopt_chroot_setup/binder/com/android/server/art/IDexoptChrootSetup.aidl @@ -22,12 +22,21 @@ interface IDexoptChrootSetup { const @utf8InCpp String CHROOT_DIR = PRE_REBOOT_DEXOPT_DIR + "/chroot"; /** - * Sets up the chroot environment. + * Sets up the chroot environment. All files in chroot, except apexes and the linker config, are + * accessible after this method is called. * * @param otaSlot The slot that contains the OTA update, "_a" or "_b", or null for a Mainline * update. + * @param mapSnapshotsForOta Whether to map/unmap snapshots. Only applicable to an OTA update. */ - void setUp(@nullable @utf8InCpp String otaSlot); + void setUp(@nullable @utf8InCpp String otaSlot, boolean mapSnapshotsForOta); + + /** + * Initializes the chroot environment. Can only be called after {@link #setUp}. Apexes and + * the linker config in chroot are accessible, and binaries are executable in chroot, after + * this method is called. + */ + void init(); /** Tears down the chroot environment. */ void tearDown(); diff --git a/dexopt_chroot_setup/dexopt_chroot_setup.cc b/dexopt_chroot_setup/dexopt_chroot_setup.cc index 35343c5b3c..d98f883df9 100644 --- a/dexopt_chroot_setup/dexopt_chroot_setup.cc +++ b/dexopt_chroot_setup/dexopt_chroot_setup.cc @@ -68,6 +68,7 @@ using ::android::base::SetProperty; using ::android::base::Split; using ::android::base::Tokenize; using ::android::base::WaitForProperty; +using ::android::base::WriteStringToFile; using ::android::fs_mgr::FstabEntry; using ::art::tools::CmdlineBuilder; using ::art::tools::Fatal; @@ -79,6 +80,10 @@ using ::ndk::ScopedAStatus; constexpr const char* kServiceName = "dexopt_chroot_setup"; const NoDestructor<std::string> kBindMountTmpDir( std::string(DexoptChrootSetup::PRE_REBOOT_DEXOPT_DIR) + "/mount_tmp"); +const NoDestructor<std::string> kOtaSlotFile(std::string(DexoptChrootSetup::PRE_REBOOT_DEXOPT_DIR) + + "/ota_slot"); +const NoDestructor<std::string> kSnapshotMappedFile( + std::string(DexoptChrootSetup::PRE_REBOOT_DEXOPT_DIR) + "/snapshot_mapped"); constexpr mode_t kChrootDefaultMode = 0755; constexpr std::chrono::milliseconds kSnapshotCtlTimeout = std::chrono::seconds(60); @@ -261,7 +266,7 @@ Result<std::vector<std::string>> GetSupportedFilesystems() { return filesystems; } -Result<void> Mount(const std::string& block_device, const std::string& target) { +Result<void> Mount(const std::string& block_device, const std::string& target, bool is_optional) { static const NoDestructor<Result<std::vector<std::string>>> supported_filesystems( GetSupportedFilesystems()); if (!supported_filesystems->ok()) { @@ -279,6 +284,10 @@ Result<void> Mount(const std::string& block_device, const std::string& target) { "Mounted '{}' at '{}' with type '{}'", block_device, target, filesystem); return {}; } else { + if (errno == ENOENT && is_optional) { + LOG(INFO) << ART_FORMAT("Skipped non-existing block device '{}'", block_device); + return {}; + } error_msgs.push_back(ART_FORMAT("Tried '{}': {}", filesystem, strerror(errno))); if (errno != EINVAL && errno != EBUSY) { // If the filesystem type is wrong, `errno` must be either `EINVAL` or `EBUSY`. For example, @@ -303,9 +312,24 @@ Result<void> MountTmpfs(const std::string& target, std::string_view se_context) return {}; } +Result<std::optional<std::string>> LoadOtaSlotFile() { + std::string content; + if (!ReadFileToString(*kOtaSlotFile, &content)) { + return ErrnoErrorf("Failed to read '{}'", *kOtaSlotFile); + } + if (content == "_a" || content == "_b") { + return content; + } + if (content.empty()) { + return std::nullopt; + } + return Errorf("Invalid content of '{}': '{}'", *kOtaSlotFile, content); +} + } // namespace -ScopedAStatus DexoptChrootSetup::setUp(const std::optional<std::string>& in_otaSlot) { +ScopedAStatus DexoptChrootSetup::setUp(const std::optional<std::string>& in_otaSlot, + bool in_mapSnapshotsForOta) { if (!mu_.try_lock()) { return Fatal("Unexpected concurrent calls"); } @@ -314,7 +338,17 @@ ScopedAStatus DexoptChrootSetup::setUp(const std::optional<std::string>& in_otaS if (in_otaSlot.has_value() && (in_otaSlot.value() != "_a" && in_otaSlot.value() != "_b")) { return Fatal(ART_FORMAT("Invalid OTA slot '{}'", in_otaSlot.value())); } - OR_RETURN_NON_FATAL(SetUpChroot(in_otaSlot)); + OR_RETURN_NON_FATAL(SetUpChroot(in_otaSlot, in_mapSnapshotsForOta)); + return ScopedAStatus::ok(); +} + +ScopedAStatus DexoptChrootSetup::init() { + if (!mu_.try_lock()) { + return Fatal("Unexpected concurrent calls"); + } + std::lock_guard<std::mutex> lock(mu_, std::adopt_lock); + + OR_RETURN_NON_FATAL(InitChroot()); return ScopedAStatus::ok(); } @@ -340,7 +374,8 @@ Result<void> DexoptChrootSetup::Start() { return {}; } -Result<void> DexoptChrootSetup::SetUpChroot(const std::optional<std::string>& ota_slot) const { +Result<void> DexoptChrootSetup::SetUpChroot(const std::optional<std::string>& ota_slot, + bool map_snapshots_for_ota) const { // Set the default permission mode for new files and dirs to be `kChrootDefaultMode`. umask(~kChrootDefaultMode & 0777); @@ -360,26 +395,43 @@ Result<void> DexoptChrootSetup::SetUpChroot(const std::optional<std::string>& ot if (!IsOtaUpdate(ota_slot)) { // Mainline update OR_RETURN(BindMount("/", CHROOT_DIR)); for (const std::string& partition : additional_system_partitions) { + // Some additional partitions are optional, but that's okay. The root filesystem (mounted at + // `/`) has empty directories for additional partitions. If additional partitions don't exist, + // we'll just be bind-mounting empty directories. OR_RETURN(BindMount("/" + partition, PathInChroot("/" + partition))); } } else { CHECK(ota_slot.value() == "_a" || ota_slot.value() == "_b"); - // Run `snapshotctl map` through init to map block devices. We can't run it ourselves because it - // requires the UID to be 0. See `sys.snapshotctl.map` in `init.rc`. - if (!SetProperty("sys.snapshotctl.map", "requested")) { - return Errorf("Failed to request snapshotctl map"); - } - if (!WaitForProperty("sys.snapshotctl.map", "finished", kSnapshotCtlTimeout)) { - return Errorf("snapshotctl timed out"); + if (map_snapshots_for_ota) { + // Write the file early in case `snapshotctl map` fails in the middle, leaving some devices + // mapped. We don't assume that `snapshotctl map` is transactional. + if (!WriteStringToFile("", *kSnapshotMappedFile)) { + return ErrnoErrorf("Failed to write '{}'", *kSnapshotMappedFile); + } + + // Run `snapshotctl map` through init to map block devices. We can't run it ourselves because + // it requires the UID to be 0. See `sys.snapshotctl.map` in `init.rc`. + if (!SetProperty("sys.snapshotctl.map", "requested")) { + return Errorf("Failed to request snapshotctl map"); + } + if (!WaitForProperty("sys.snapshotctl.map", "finished", kSnapshotCtlTimeout)) { + return Errorf("snapshotctl timed out"); + } + + // We don't know whether snapshotctl succeeded or not, but if it failed, the mount operation + // below will fail with `ENOENT`. + OR_RETURN( + Mount(GetBlockDeviceName("system", ota_slot.value()), CHROOT_DIR, /*is_optional=*/false)); + } else { + // update_engine has mounted `system` at `/postinstall` for us. + OR_RETURN(BindMount("/postinstall", CHROOT_DIR)); } - // We don't know whether snapshotctl succeeded or not, but if it failed, the mount operation - // below will fail with `ENOENT`. - OR_RETURN(Mount(GetBlockDeviceName("system", ota_slot.value()), CHROOT_DIR)); for (const std::string& partition : additional_system_partitions) { - OR_RETURN( - Mount(GetBlockDeviceName(partition, ota_slot.value()), PathInChroot("/" + partition))); + OR_RETURN(Mount(GetBlockDeviceName(partition, ota_slot.value()), + PathInChroot("/" + partition), + /*is_optional=*/true)); } } @@ -406,6 +458,16 @@ Result<void> DexoptChrootSetup::SetUpChroot(const std::optional<std::string>& ot OR_RETURN(BindMountRecursive(src, PathInChroot(src))); } + if (!WriteStringToFile(ota_slot.value_or(""), *kOtaSlotFile)) { + return ErrnoErrorf("Failed to write '{}'", *kOtaSlotFile); + } + + return {}; +} + +Result<void> DexoptChrootSetup::InitChroot() const { + std::optional<std::string> ota_slot = OR_RETURN(LoadOtaSlotFile()); + // Generate empty linker config to suppress warnings. if (!android::base::WriteStringToFile("", PathInChroot("/linkerconfig/ld.config.txt"))) { PLOG(WARNING) << "Failed to generate empty linker config to suppress warnings"; @@ -482,11 +544,22 @@ Result<void> DexoptChrootSetup::TearDownChroot() const { return Errorf("Failed to remove dir '{}': {}", *kBindMountTmpDir, ec.message()); } - if (!SetProperty("sys.snapshotctl.unmap", "requested")) { - return Errorf("Failed to request snapshotctl unmap"); + std::filesystem::remove(*kOtaSlotFile, ec); + if (ec) { + return Errorf("Failed to remove file '{}': {}", *kOtaSlotFile, ec.message()); } - if (!WaitForProperty("sys.snapshotctl.unmap", "finished", kSnapshotCtlTimeout)) { - return Errorf("snapshotctl timed out"); + + if (OS::FileExists(kSnapshotMappedFile->c_str())) { + if (!SetProperty("sys.snapshotctl.unmap", "requested")) { + return Errorf("Failed to request snapshotctl unmap"); + } + if (!WaitForProperty("sys.snapshotctl.unmap", "finished", kSnapshotCtlTimeout)) { + return Errorf("snapshotctl timed out"); + } + std::filesystem::remove(*kSnapshotMappedFile, ec); + if (ec) { + return Errorf("Failed to remove file '{}': {}", *kSnapshotMappedFile, ec.message()); + } } return {}; diff --git a/dexopt_chroot_setup/dexopt_chroot_setup.h b/dexopt_chroot_setup/dexopt_chroot_setup.h index baceb1a480..de0e35fa4d 100644 --- a/dexopt_chroot_setup/dexopt_chroot_setup.h +++ b/dexopt_chroot_setup/dexopt_chroot_setup.h @@ -30,15 +30,20 @@ namespace dexopt_chroot_setup { // A service that sets up the chroot environment for Pre-reboot Dexopt. class DexoptChrootSetup : public aidl::com::android::server::art::BnDexoptChrootSetup { public: - ndk::ScopedAStatus setUp(const std::optional<std::string>& in_otaSlot) override; + ndk::ScopedAStatus setUp(const std::optional<std::string>& in_otaSlot, + bool in_mapSnapshotsForOta) override; + + ndk::ScopedAStatus init() override; ndk::ScopedAStatus tearDown() override; android::base::Result<void> Start(); private: - android::base::Result<void> SetUpChroot(const std::optional<std::string>& ota_slot) const - REQUIRES(mu_); + android::base::Result<void> SetUpChroot(const std::optional<std::string>& ota_slot, + bool map_snapshots_for_ota) const REQUIRES(mu_); + + android::base::Result<void> InitChroot() const REQUIRES(mu_); android::base::Result<void> TearDownChroot() const REQUIRES(mu_); diff --git a/dexopt_chroot_setup/dexopt_chroot_setup_test.cc b/dexopt_chroot_setup/dexopt_chroot_setup_test.cc index bfa38e0aa0..8e8cfef754 100644 --- a/dexopt_chroot_setup/dexopt_chroot_setup_test.cc +++ b/dexopt_chroot_setup/dexopt_chroot_setup_test.cc @@ -102,7 +102,9 @@ class DexoptChrootSetupTest : public CommonArtTest { TEST_F(DexoptChrootSetupTest, Run) { // We only test the Mainline update case here. There isn't an easy way to test the OTA update case // in such a unit test. The OTA update case is assumed to be covered by the E2E test. - ASSERT_STATUS_OK(dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt)); + ASSERT_STATUS_OK( + dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt, /*in_mapSnapshotsForOta=*/false)); + ASSERT_STATUS_OK(dexopt_chroot_setup_->init()); // Some important dirs that should be the same as outside. std::vector<const char*> same_dirs = { @@ -172,7 +174,9 @@ TEST_F(DexoptChrootSetupTest, Run) { // Check that `setUp` can be repetitively called, to simulate the case where an instance of the // caller (typically system_server) called `setUp` and crashed later, and a new instance called // `setUp` again. - ASSERT_STATUS_OK(dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt)); + ASSERT_STATUS_OK( + dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt, /*in_mapSnapshotsForOta=*/false)); + ASSERT_STATUS_OK(dexopt_chroot_setup_->init()); ASSERT_STATUS_OK(dexopt_chroot_setup_->tearDown()); @@ -180,6 +184,12 @@ TEST_F(DexoptChrootSetupTest, Run) { // Check that `tearDown` can be repetitively called too. ASSERT_STATUS_OK(dexopt_chroot_setup_->tearDown()); + + // Check that `setUp` can be followed directly by a `tearDown`. + ASSERT_STATUS_OK( + dexopt_chroot_setup_->setUp(/*in_otaSlot=*/std::nullopt, /*in_mapSnapshotsForOta=*/false)); + ASSERT_STATUS_OK(dexopt_chroot_setup_->tearDown()); + EXPECT_FALSE(std::filesystem::exists(DexoptChrootSetup::CHROOT_DIR)); } } // namespace diff --git a/libartbase/base/common_art_test.cc b/libartbase/base/common_art_test.cc index 0692bf8c20..54069fad2e 100644 --- a/libartbase/base/common_art_test.cc +++ b/libartbase/base/common_art_test.cc @@ -199,8 +199,6 @@ std::string CommonArtTestImpl::GetAndroidBuildTop() { if (android_build_top_from_env != nullptr) { if (std::filesystem::weakly_canonical(android_build_top).string() != std::filesystem::weakly_canonical(android_build_top_from_env).string()) { - LOG(WARNING) << "Execution path (" << argv << ") not below ANDROID_BUILD_TOP (" - << android_build_top_from_env << ")! Using env-var."; android_build_top = android_build_top_from_env; } } else { @@ -247,6 +245,26 @@ std::string CommonArtTestImpl::GetAndroidHostOut() { return expected.string(); } +std::string CommonArtTestImpl::GetHostBootClasspathInstallRoot() { + CHECK(IsHost()); + std::string build_install_root = GetAndroidHostOut() + "/testcases/art_common/out/host/linux-x86"; + // Look for the `apex` subdirectory as a discriminator to check the location. + if (OS::DirectoryExists((build_install_root + "/apex").c_str())) { + // This is the path where "m art-host-tests" installs support files for host + // tests, so use it when the tests are run in a build tree (which is the + // case when testing locally). + return build_install_root; + } + if (OS::DirectoryExists((GetAndroidRoot() + "/apex").c_str())) { + // This is the location for host tests in CI when the files are unzipped + // from art-host-tests.zip. + return GetAndroidRoot(); + } + LOG(ERROR) << "Neither location has a boot classpath (forgot \"m art-host-tests\"?): " + << build_install_root << " or " << GetAndroidRoot(); + return "<no boot classpath found>"; +} + void CommonArtTestImpl::SetUpAndroidRootEnvVars() { if (IsHost()) { std::string android_host_out = GetAndroidHostOut(); @@ -448,12 +466,14 @@ std::vector<std::string> CommonArtTestImpl::GetLibCoreModuleNames() const { std::vector<std::string> CommonArtTestImpl::GetLibCoreDexFileNames( const std::vector<std::string>& modules) const { - return art::testing::GetLibCoreDexFileNames(kIsTargetBuild ? "" : GetAndroidRoot(), modules); + return art::testing::GetLibCoreDexFileNames( + kIsTargetBuild ? "" : GetHostBootClasspathInstallRoot(), modules); } std::vector<std::string> CommonArtTestImpl::GetLibCoreDexFileNames() const { std::vector<std::string> modules = GetLibCoreModuleNames(); - return art::testing::GetLibCoreDexFileNames(kIsTargetBuild ? "" : GetAndroidRoot(), modules); + return art::testing::GetLibCoreDexFileNames( + kIsTargetBuild ? "" : GetHostBootClasspathInstallRoot(), modules); } std::vector<std::string> CommonArtTestImpl::GetLibCoreDexLocations( @@ -535,9 +555,7 @@ std::unique_ptr<const DexFile> CommonArtTestImpl::OpenTestDexFile(const char* na std::string CommonArtTestImpl::GetImageDirectory() { if (IsHost()) { - const char* host_dir = getenv("ANDROID_HOST_OUT"); - CHECK(host_dir != nullptr); - return std::string(host_dir) + "/apex/art_boot_images/javalib"; + return GetHostBootClasspathInstallRoot() + "/apex/art_boot_images/javalib"; } // On device, the boot image is generated by `generate-boot-image`. // In a standalone test, the boot image is located next to the gtest binary itself. diff --git a/libartbase/base/common_art_test.h b/libartbase/base/common_art_test.h index c490e085cd..f07f1bfc89 100644 --- a/libartbase/base/common_art_test.h +++ b/libartbase/base/common_art_test.h @@ -237,6 +237,12 @@ class CommonArtTestImpl { // Returns ${ANDROID_HOST_OUT}. static std::string GetAndroidHostOut(); + // Returns the path where boot classpath and boot image files are installed + // for host tests (by the art_common mk module, typically built through "m + // art-host-tests"). Different in CI where they are unpacked from the + // art-host-tests.zip file. + static std::string GetHostBootClasspathInstallRoot(); + // File location to boot.art, e.g. /apex/com.android.art/javalib/boot.art static std::string GetCoreArtLocation(); diff --git a/libartbase/base/debugstore.h b/libartbase/base/debugstore.h new file mode 100644 index 0000000000..b3e6563c60 --- /dev/null +++ b/libartbase/base/debugstore.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_LIBARTBASE_BASE_DEBUGSTORE_H_ +#define ART_LIBARTBASE_BASE_DEBUGSTORE_H_ + +#include <array> +#include <string> + +#include "palette/palette.h" + +namespace art { +static constexpr size_t STORE_MAX_SIZE = 1024; + +inline std::string DebugStoreGetString() { + std::array<char, STORE_MAX_SIZE> result{}; + // If PaletteDebugStoreGetString returns PALETTE_STATUS_NOT_SUPPORTED, + // set an empty string as the result. + result[0] = '\0'; + PaletteDebugStoreGetString(result.data(), result.size()); + return std::string(result.data()); +} + +} // namespace art + +#endif // ART_LIBARTBASE_BASE_DEBUGSTORE_H_ diff --git a/libartbase/base/memory_tool.h b/libartbase/base/memory_tool.h index 675ceb2cfd..4a93a9642f 100644 --- a/libartbase/base/memory_tool.h +++ b/libartbase/base/memory_tool.h @@ -68,10 +68,12 @@ constexpr size_t kMemoryToolStackGuardSizeScale = 1; #if __has_feature(hwaddress_sanitizer) # define HWADDRESS_SANITIZER +constexpr bool kHwAsanEnabled = true; // NB: The attribute also implies NO_INLINE. If inlined, the hwasan attribute would be lost. // If method is also separately marked as ALWAYS_INLINE, the NO_INLINE takes precedence. # define ATTRIBUTE_NO_SANITIZE_HWADDRESS __attribute__((no_sanitize("hwaddress"), noinline)) #else +constexpr bool kHwAsanEnabled = false; # define ATTRIBUTE_NO_SANITIZE_HWADDRESS #endif diff --git a/libartbase/base/pidfd.h b/libartbase/base/pidfd.h new file mode 100644 index 0000000000..d209d931c8 --- /dev/null +++ b/libartbase/base/pidfd.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_LIBARTBASE_BASE_PIDFD_H_ +#define ART_LIBARTBASE_BASE_PIDFD_H_ + +#include <cstdint> +#include <cstdio> + +#include "android-base/unique_fd.h" + +#ifdef __BIONIC__ +#include <sys/pidfd.h> +#endif + +namespace art { + +[[maybe_unused]] static android::base::unique_fd PidfdOpen(pid_t pid, uint32_t flags) { +#ifdef __BIONIC__ + return android::base::unique_fd(pidfd_open(pid, flags)); +#else + // There is no glibc wrapper for pidfd_open. +#ifndef SYS_pidfd_open + constexpr int SYS_pidfd_open = 434; +#endif + return android::base::unique_fd(syscall(SYS_pidfd_open, pid, flags)); +#endif +} + +} // namespace art + +#endif // ART_LIBARTBASE_BASE_PIDFD_H_ diff --git a/libartpalette/apex/palette.cc b/libartpalette/apex/palette.cc index 346cd184ff..3efee45173 100644 --- a/libartpalette/apex/palette.cc +++ b/libartpalette/apex/palette.cc @@ -254,4 +254,11 @@ palette_status_t PaletteSetTaskProfiles(int32_t tid, return m(tid, profiles, profiles_len); } +// Methods in version 4 API, corresponding to SDK level 36. +palette_status_t PaletteDebugStoreGetString(char* result, size_t max_size) { + PaletteDebugStoreGetStringMethod m = + PaletteLoader::Instance().GetPaletteDebugStoreGetStringMethod(); + return m(result, max_size); +} + } // extern "C" diff --git a/libartpalette/apex/palette_test.cc b/libartpalette/apex/palette_test.cc index 63072c491b..cbdfafa733 100644 --- a/libartpalette/apex/palette_test.cc +++ b/libartpalette/apex/palette_test.cc @@ -21,6 +21,7 @@ #include <sys/syscall.h> #include <unistd.h> +#include <cstring> #include <filesystem> #ifdef ART_TARGET_ANDROID @@ -50,6 +51,10 @@ bool PaletteSetTaskProfilesIsSupported(palette_status_t res) { << "Device API level: " << android_get_device_api_level(); return false; } +bool PaletteDebugStoreIsSupported() { + // TODO(b/345433959): Switch to android::modules::sdklevel::IsAtLeastW + return android_get_device_api_level() >= 36; +} #endif } // namespace @@ -166,3 +171,25 @@ TEST_F(PaletteClientTest, SetTaskProfilesCpp) { } #endif } + +TEST_F(PaletteClientTest, DebugStore) { +#ifndef ART_TARGET_ANDROID + GTEST_SKIP() << "DebugStore is only supported on Android"; +#else + std::array<char, 20> result{}; + // Make sure the we are on a correct API level. + if (!PaletteDebugStoreIsSupported()) { + GTEST_SKIP() << "DebugStore is only supported on API 36+"; + } + palette_status_t pstatus = PaletteDebugStoreGetString(result.data(), result.size()); + EXPECT_EQ(PALETTE_STATUS_OK, pstatus); + + size_t len = strnlen(result.data(), result.size()); + EXPECT_TRUE(len < result.size()); + + const char* start = "1,0,"; + const char* end = "::"; + EXPECT_TRUE(len > strlen(start) + strlen(end)); + EXPECT_EQ(strncmp(result.data() + len - strlen(end), end, strlen(end)), 0); +#endif +} diff --git a/libartpalette/include/palette/palette_method_list.h b/libartpalette/include/palette/palette_method_list.h index e5437cb33a..b2f0476892 100644 --- a/libartpalette/include/palette/palette_method_list.h +++ b/libartpalette/include/palette/palette_method_list.h @@ -75,6 +75,23 @@ /* PALETTE_STATUS_NOT_SUPPORTED if the implementation no longer supports this */ \ /* call. This can happen at any future SDK level since this function wraps an */ \ /* internal unstable API. */ \ - M(PaletteSetTaskProfiles, int32_t tid, const char* const profiles[], size_t profiles_len) + M(PaletteSetTaskProfiles, int32_t tid, const char* const profiles[], size_t profiles_len) \ + \ + /* Methods in version 4 API, corresponding to SDK level 36. */ \ + \ + /* Retrieves the debug store as a string. */ \ + /* */ \ + /* This function retrieves debug information stored in a predefined debug store. */ \ + /* The information retrieved are used for debugging and logging purposes. */ \ + /* */ \ + /* @param result A pointer to a null-terminated character array where the retrieved */ \ + /* string will be stored. */ \ + /* @param max_size The maximum number of characters to be copied into the result, */ \ + /* including the null terminator. It is the caller's responsibility */ \ + /* to ensure that the pointed by result is large enough to store */ \ + /* up to max_size characters. */ \ + /* @return PALETTE_STATUS_OK if the call succeeded. */ \ + /* PALETTE_STATUS_INVALID_ARGUMENT if the pointer is a nullptr or max_size is 0 */ \ + M(PaletteDebugStoreGetString, char* result, size_t max_size) #endif // ART_LIBARTPALETTE_INCLUDE_PALETTE_PALETTE_METHOD_LIST_H_ diff --git a/libartpalette/libartpalette.map b/libartpalette/libartpalette.map index 6172a21427..e3c36f5277 100644 --- a/libartpalette/libartpalette.map +++ b/libartpalette/libartpalette.map @@ -51,3 +51,9 @@ LIBARTPALETTE_3 { # introduced=34 # --- VERSION 03 API --- PaletteSetTaskProfiles; # apex } LIBARTPALETTE_2; + +LIBARTPALETTE_4 { # introduced=36 + global: + # --- VERSION 04 API --- + PaletteDebugStoreGetString; # apex +} LIBARTPALETTE_3;
\ No newline at end of file diff --git a/libartpalette/system/palette_fake.cc b/libartpalette/system/palette_fake.cc index e698267d10..f2fc76d68c 100644 --- a/libartpalette/system/palette_fake.cc +++ b/libartpalette/system/palette_fake.cc @@ -147,3 +147,9 @@ palette_status_t PaletteSetTaskProfiles([[maybe_unused]] int32_t tid, [[maybe_unused]] size_t profiles_len) { return PALETTE_STATUS_OK; } + +// Methods in version 4 API, corresponding to SDK level 36. +palette_status_t PaletteDebugStoreGetString([[maybe_unused]] char* result, + [[maybe_unused]] size_t max_size) { + return PALETTE_STATUS_OK; +} diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp index ef28353666..0b1bd90c20 100644 --- a/libartservice/service/Android.bp +++ b/libartservice/service/Android.bp @@ -32,6 +32,7 @@ cc_defaults { ], export_include_dirs: ["native"], shared_libs: [ + "liblog", // Used by "JNIHelp.h". "libnativehelper", ], } diff --git a/libartservice/service/java/com/android/server/art/AidlUtils.java b/libartservice/service/java/com/android/server/art/AidlUtils.java index 6531e61668..055014fb43 100644 --- a/libartservice/service/java/com/android/server/art/AidlUtils.java +++ b/libartservice/service/java/com/android/server/art/AidlUtils.java @@ -53,6 +53,14 @@ public final class AidlUtils { } @NonNull + public static ArtifactsPath buildArtifactsPathAsInputPreReboot( + @NonNull String dexPath, @NonNull String isa, boolean isInDalvikCache) { + // Normally this should not be called, unless the caller really means to use Pre-reboot + // artifacts as inputs. + return buildArtifactsPath(dexPath, isa, isInDalvikCache, true /* isPreReboot */); + } + + @NonNull public static FsPermission buildFsPermission( int uid, int gid, boolean isOtherReadable, boolean isOtherExecutable) { var fsPermission = new FsPermission(); diff --git a/libartservice/service/java/com/android/server/art/ArtJni.java b/libartservice/service/java/com/android/server/art/ArtJni.java index d46b889460..62b53dd6c8 100644 --- a/libartservice/service/java/com/android/server/art/ArtJni.java +++ b/libartservice/service/java/com/android/server/art/ArtJni.java @@ -25,6 +25,8 @@ import androidx.annotation.RequiresApi; import dalvik.system.VMRuntime; +import java.io.IOException; + /** * JNI methods for ART Service with wrappers. * @@ -112,9 +114,59 @@ public class ArtJni { return getGarbageCollectorNative(); } + /** + * Sets the system property {@code key} to {@code value}. + * + * @throws IllegalStateException if the operation fails. This caller should not expect this, + * unless the inputs are invalid (e.g., value too long). + */ + public static Void setProperty(@NonNull String key, @NonNull String value) { + if (GlobalInjector.getInstance().isPreReboot()) { + // We don't need this for Pre-reboot Dexopt. + throw new UnsupportedOperationException(); + } + setPropertyNative(key, value); + // Return a placeholder value to make this method easier to mock. There is no good way to + // mock a method that is both void and static, due to the poor design of Mockito API. + return null; + } + + /** + * Waits for processes whose executable is in the given directory to exit, and kills them if + * they don't exit within the timeout. + * + * Note that this method only checks processes' executable paths, not their open files. If the + * executable of a process is outside of the given directory but the process opens a file in + * that directory, this method doesn't handle it. + * + * After killing, the method waits another round with the given timeout. Theoretically, this + * method can take at most {@code 2 * timeoutMs}. However, the second round should be pretty + * fast in practice. + * + * This method assumes that no new process is started from an executable in the given directory + * while the method is running. It is the callers responsibility to make sure that this + * assumption holds. + * + * @throws IllegalArgumentException if {@code timeoutMs} is negative + * @throws IOException if the operation fails + */ + public static Void ensureNoProcessInDir(@NonNull String dir, int timeoutMs) throws IOException { + if (GlobalInjector.getInstance().isPreReboot()) { + // We don't need this for Pre-reboot Dexopt. + throw new UnsupportedOperationException(); + } + ensureNoProcessInDirNative(dir, timeoutMs); + // Return a placeholder value to make this method easier to mock. There is no good way to + // mock a method that is both void and static, due to the poor design of Mockito API. + return null; + } + @Nullable private static native String validateDexPathNative(@NonNull String dexPath); @Nullable private static native String validateClassLoaderContextNative( @NonNull String dexPath, @NonNull String classLoaderContext); @NonNull private static native String getGarbageCollectorNative(); + private static native void setPropertyNative(@NonNull String key, @NonNull String value); + private static native void ensureNoProcessInDirNative(@NonNull String dir, int timeoutMs) + throws IOException; } diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java index ffbd66a99a..8be328ca9a 100644 --- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java +++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java @@ -74,6 +74,7 @@ import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; import com.android.server.art.model.DexoptStatus; import com.android.server.art.model.OperationProgress; +import com.android.server.art.prereboot.PreRebootStatsReporter; import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.AndroidPackageSplit; @@ -130,6 +131,10 @@ public final class ArtManagerLocal { private boolean mShouldCommitPreRebootStagedFiles = false; + // A temporary object for holding stats while staged files are being committed, used in two + // places: `onBoot` and the `BroadcastReceiver` of `ACTION_BOOT_COMPLETED`. + @Nullable private PreRebootStatsReporter.AfterRebootSession mStatsAfterRebootSession = null; + @Deprecated public ArtManagerLocal() { mInjector = new Injector(); @@ -876,6 +881,8 @@ public final class ArtManagerLocal { // to commit files for secondary dex files because they are not decrypted before // then. mShouldCommitPreRebootStagedFiles = true; + mStatsAfterRebootSession = + mInjector.getPreRebootStatsReporter().new AfterRebootSession(); commitPreRebootStagedFiles(snapshot, false /* forSecondary */); } dexoptPackages(snapshot, bootReason, new CancellationSignal(), progressCallbackExecutor, @@ -903,6 +910,8 @@ public final class ArtManagerLocal { try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) { commitPreRebootStagedFiles(snapshot, true /* forSecondary */); } + mStatsAfterRebootSession.reportAsync(); + mStatsAfterRebootSession = null; } }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); } @@ -923,13 +932,7 @@ public final class ArtManagerLocal { @SuppressLint("UnflaggedApi") // Flag support for mainline is not available. @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) public void onApexStaged(@NonNull String[] stagedApexModuleNames) { - // TODO(b/311377497): Check system requirements. - mInjector.getPreRebootDexoptJob().unschedule(); - // Although `unschedule` implies `cancel`, we explicitly call `cancel` here to wait for - // the job to exit, if it's running. - mInjector.getPreRebootDexoptJob().cancel(true /* blocking */); - mInjector.getPreRebootDexoptJob().updateOtaSlot(null); - mInjector.getPreRebootDexoptJob().schedule(); + mInjector.getPreRebootDexoptJob().onUpdateReady(null /* otaSlot */); } /** @@ -1105,7 +1108,7 @@ public final class ArtManagerLocal { @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) private void commitPreRebootStagedFiles( @NonNull PackageManagerLocal.FilteredSnapshot snapshot, boolean forSecondary) { - try { + try (var pin = mInjector.createArtdPin()) { // Because we don't know for which packages the Pre-reboot Dexopt job has generated // staged files, we call artd for all dexoptable packages, which is a superset of the // packages that we actually expect to have staged files. @@ -1138,7 +1141,10 @@ public final class ArtManagerLocal { // ones generated by background dexopt, committing the artifacts while failing // to commit the profile can potentially cause a permanent performance // regression. - mInjector.getArtd().commitPreRebootStagedFiles(artifacts, profiles); + if (mInjector.getArtd().commitPreRebootStagedFiles(artifacts, profiles)) { + mStatsAfterRebootSession.recordPackageWithArtifacts( + pkgState.getPackageName()); + } } catch (ServiceSpecificException e) { AsLog.e("Failed to commit Pre-reboot staged files for package '" + pkgState.getPackageName() + "'", @@ -1650,5 +1656,11 @@ public final class ArtManagerLocal { public DexMetadataHelper getDexMetadataHelper() { return new DexMetadataHelper(); } + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull + public PreRebootStatsReporter getPreRebootStatsReporter() { + return new PreRebootStatsReporter(); + } } } diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java index ee1aa88b50..7d8d7dcaa3 100644 --- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java +++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java @@ -60,11 +60,15 @@ import com.android.server.pm.pkg.PackageState; import libcore.io.Streams; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -183,13 +187,12 @@ public final class ArtShellCommand extends BasicShellCommandHandler { pw.println("Profiles cleared"); return 0; } - case "pre-reboot-dexopt": { - // TODO(b/311377497): Remove this command once the development is done. - return handlePreRebootDexopt(pw); - } case "on-ota-staged": { return handleOnOtaStaged(pw); } + case "pr-dexopt-job": { + return handlePrDexoptJob(pw); + } default: pw.printf("Error: Unknown 'art' sub-command '%s'\n", subcmd); pw.println("See 'pm help' for help"); @@ -646,33 +649,6 @@ public final class ArtShellCommand extends BasicShellCommandHandler { return 0; } - private int handlePreRebootDexopt(@NonNull PrintWriter pw) { - if (!SdkLevel.isAtLeastV()) { - pw.println("Error: Unsupported command 'pre-reboot-dexopt'"); - return 1; - } - - String otaSlot = null; - - String opt = getNextOption(); - if ("--slot".equals(opt)) { - otaSlot = getNextArgRequired(); - } else if (opt != null) { - pw.println("Error: Unknown option: " + opt); - return 1; - } - - try (var signal = new WithCancellationSignal(pw, true /* verbose */)) { - if (new PreRebootDriver(mContext).run(otaSlot, signal.get())) { - pw.println("Job finished. See logs for details"); - return 0; - } else { - pw.println("Job failed. See logs for details"); - return 1; - } - } - } - private int handleOnOtaStaged(@NonNull PrintWriter pw) { if (!SdkLevel.isAtLeastV()) { pw.println("Error: Unsupported command 'on-ota-staged'"); @@ -703,30 +679,110 @@ public final class ArtShellCommand extends BasicShellCommandHandler { return 1; } - int code; + if (mArtManagerLocal.getPreRebootDexoptJob().isAsyncForOta()) { + return handleSchedulePrDexoptJob(pw, otaSlot); + } else { + return handleRunPrDexoptJob(pw, otaSlot); + } + } + + private int handlePrDexoptJob(@NonNull PrintWriter pw) { + if (!SdkLevel.isAtLeastV()) { + pw.println("Error: Unsupported command 'pr-dexopt-job'"); + return 1; + } + + boolean isTest = false; + + String opt = getNextOption(); + if ("--test".equals(opt)) { + isTest = true; + } else if (opt != null) { + pw.println("Error: Unknown option: " + opt); + return 1; + } + + if (isTest) { + try { + mArtManagerLocal.getPreRebootDexoptJob().test(); + pw.println("Success"); + return 0; + } catch (Exception e) { + pw.println("Failure"); + e.printStackTrace(pw); + return 2; // "1" is for general errors. Use "2" for the test failure. + } + } + + pw.println("Error: No option specified"); + return 1; + } + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private int handleRunPrDexoptJob(@NonNull PrintWriter pw, @Nullable String otaSlot) { + PreRebootDexoptJob job = mArtManagerLocal.getPreRebootDexoptJob(); + + CompletableFuture<Void> future = job.onUpdateReadyStartNow(otaSlot); + if (future == null) { + pw.println("Job disabled by system property"); + return 1; + } + + // Put the read in a separate thread because there isn't an easy way in Java to wait for + // both the `Future` and the read. + var readThread = new Thread(() -> { + try (var in = new FileInputStream(getInFileDescriptor())) { + ByteBuffer buffer = ByteBuffer.allocate(128 /* capacity */); + FileChannel channel = in.getChannel(); + while (channel.read(buffer) >= 0) { + buffer.clear(); + } + // Broken pipe. + job.cancelGiven(future, true /* expectInterrupt */); + } catch (ClosedByInterruptException e) { + // Job finished normally. + } catch (IOException e) { + AsLog.e("Unexpected exception", e); + job.cancelGiven(future, true /* expectInterrupt */); + } catch (RuntimeException e) { + AsLog.wtf("Unexpected exception", e); + job.cancelGiven(future, true /* expectInterrupt */); + } + }); + readThread.start(); + pw.println("Job running... To cancel it, press Ctrl+C."); + pw.flush(); - // This operation requires the uid to be "system" (1000). - long identityToken = Binder.clearCallingIdentity(); try { - mArtManagerLocal.getPreRebootDexoptJob().unschedule(); - // Although `unschedule` implies `cancel`, we explicitly call `cancel` here to wait for - // the job to exit, if it's running. - mArtManagerLocal.getPreRebootDexoptJob().cancel(true /* blocking */); - mArtManagerLocal.getPreRebootDexoptJob().updateOtaSlot(otaSlot); - code = mArtManagerLocal.getPreRebootDexoptJob().schedule(); + Utils.getFuture(future); + pw.println("Job finished. See logs for details"); + } catch (RuntimeException e) { + pw.println("Job encountered a fatal error"); + e.printStackTrace(pw); } finally { - Binder.restoreCallingIdentity(identityToken); + readThread.interrupt(); + try { + readThread.join(); + } catch (InterruptedException e) { + AsLog.wtf("Interrupted", e); + } } + return 0; + } + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private int handleSchedulePrDexoptJob(@NonNull PrintWriter pw, @Nullable String otaSlot) { + int code = mArtManagerLocal.getPreRebootDexoptJob().onUpdateReady(otaSlot); switch (code) { case ArtFlags.SCHEDULE_SUCCESS: - pw.println("Job scheduled"); + pw.println("Pre-reboot Dexopt job scheduled"); return 0; case ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP: - pw.println("Job disabled by system property"); + pw.println("Pre-reboot Dexopt job disabled by system property"); return 1; case ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE: - pw.println("Failed to schedule job"); + pw.println("Failed to schedule Pre-reboot Dexopt job"); return 1; default: // Can't happen. @@ -885,8 +941,16 @@ public final class ArtShellCommand extends BasicShellCommandHandler { pw.println(" API documentation for 'ArtManagerLocal.dexoptPackages' for details."); pw.println(); pw.println(" on-ota-staged --slot SLOT"); - pw.println(" Notifies ART Service that an OTA update is staged. A Pre-reboot Dexopt"); - pw.println(" job is scheduled for it."); + pw.println(" Notifies ART Service that an OTA update is staged. ART Service decides"); + pw.println(" what to do with this notification:"); + pw.println(" - If Pre-reboot Dexopt is disabled or unsupported, the command returns"); + pw.println(" non-zero."); + pw.println(" - If Pre-reboot Dexopt is enabled in synchronous mode, the command blocks"); + pw.println(" until Pre-reboot Dexopt finishes, and returns zero no matter it"); + pw.println(" succeeds or not."); + pw.println(" - If Pre-reboot Dexopt is enabled in asynchronous mode, the command"); + pw.println(" schedules an asynchronous job and returns 0 immediately. The job will"); + pw.println(" then run by the job scheduler when the device is idle and charging."); pw.println(" Options:"); pw.println(" --slot SLOT The slot that contains the OTA update, '_a' or '_b'."); } diff --git a/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java b/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java index 6dda5c9980..526a08eb48 100644 --- a/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java +++ b/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java @@ -183,7 +183,7 @@ public class BackgroundDexoptJob implements ArtServiceJobInterface { try (var tracing = new Utils.TracingWithTimingLogging(AsLog.getTag(), "jobExecution")) { return run(mCancellationSignal); } catch (RuntimeException e) { - AsLog.e("Fatal error", e); + AsLog.wtf("Fatal error", e); return new FatalErrorResult(); } finally { synchronized (this) { diff --git a/libartservice/service/java/com/android/server/art/DexoptHelper.java b/libartservice/service/java/com/android/server/art/DexoptHelper.java index 76edfd503f..1f7bd93235 100644 --- a/libartservice/service/java/com/android/server/art/DexoptHelper.java +++ b/libartservice/service/java/com/android/server/art/DexoptHelper.java @@ -141,21 +141,37 @@ public class DexoptHelper { PackageState pkgState = pkgStates.get(i); CancellationSignal childCancellationSignal = childCancellationSignals.get(i); futures.add(CompletableFuture.supplyAsync(() -> { - return dexoptPackage(pkgState, params, childCancellationSignal); + AndroidPackage pkg = Utils.getPackageOrThrow(pkgState); + if (canDexoptPackage(pkgState) + && (params.getFlags() & ArtFlags.FLAG_FOR_SINGLE_SPLIT) != 0) { + // Throws if the split is not found. + PrimaryDexUtils.getDexInfoBySplitName(pkg, params.getSplitName()); + } + try { + return dexoptPackage(pkgState, pkg, params, childCancellationSignal); + } catch (RuntimeException e) { + AsLog.wtf("Unexpected package-level exception during dexopt", e); + return PackageDexoptResult.create(pkgState.getPackageName(), + new ArrayList<>() /* dexContainerFileDexoptResults */, + DexoptResult.DEXOPT_FAILED); + } }, dexoptExecutor)); } if (progressCallback != null) { CompletableFuture.runAsync(() -> { - progressCallback.accept( - OperationProgress.create(0 /* current */, futures.size())); + progressCallback.accept(OperationProgress.create( + 0 /* current */, futures.size(), null /* packageDexoptResult */)); }, progressCallbackExecutor); AtomicInteger current = new AtomicInteger(0); for (CompletableFuture<PackageDexoptResult> future : futures) { - future.thenRunAsync(() -> { - progressCallback.accept(OperationProgress.create( - current.incrementAndGet(), futures.size())); - }, progressCallbackExecutor); + future.thenAcceptAsync(result -> { + progressCallback.accept(OperationProgress.create( + current.incrementAndGet(), futures.size(), result)); + }, progressCallbackExecutor).exceptionally(t -> { + AsLog.e("Failed to update progress", t); + return null; + }); } } @@ -201,23 +217,17 @@ public class DexoptHelper { */ @NonNull private PackageDexoptResult dexoptPackage(@NonNull PackageState pkgState, - @NonNull DexoptParams params, @NonNull CancellationSignal cancellationSignal) { + @NonNull AndroidPackage pkg, @NonNull DexoptParams params, + @NonNull CancellationSignal cancellationSignal) { List<DexContainerFileDexoptResult> results = new ArrayList<>(); Function<Integer, PackageDexoptResult> createResult = (packageLevelStatus) -> PackageDexoptResult.create( pkgState.getPackageName(), results, packageLevelStatus); - AndroidPackage pkg = Utils.getPackageOrThrow(pkgState); - if (!canDexoptPackage(pkgState)) { return createResult.apply(null /* packageLevelStatus */); } - if ((params.getFlags() & ArtFlags.FLAG_FOR_SINGLE_SPLIT) != 0) { - // Throws if the split is not found. - PrimaryDexUtils.getDexInfoBySplitName(pkg, params.getSplitName()); - } - try (var tracing = new Utils.Tracing("dexopt")) { if ((params.getFlags() & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) { if (cancellationSignal.isCanceled()) { diff --git a/libartservice/service/java/com/android/server/art/Dexopter.java b/libartservice/service/java/com/android/server/art/Dexopter.java index b8aa912723..271dc6144f 100644 --- a/libartservice/service/java/com/android/server/art/Dexopter.java +++ b/libartservice/service/java/com/android/server/art/Dexopter.java @@ -118,6 +118,14 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { continue; } + if (mInjector.isPreReboot() && !isDexFileFound(dexInfo)) { + // In the pre-reboot case, it's possible that a dex file doesn't exist in the + // new system image. Although code below can gracefully handle failures, those + // failures can be red herrings in metrics and bug reports, so we skip + // non-existing dex files to avoid them. + continue; + } + DexMetadataInfo dmInfo = mInjector.getDexMetadataHelper().getDexMetadataInfo(buildDmPath(dexInfo)); @@ -200,6 +208,27 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { .setNeedsToBePublic(needsToBeShared) .build(); + if (mInjector.isPreReboot()) { + ArtifactsPath existingArtifacts = + AidlUtils.buildArtifactsPathAsInputPreReboot( + target.dexInfo().dexPath(), target.isa(), + target.isInDalvikCache()); + if (mInjector.getArtd().getArtifactsVisibility(existingArtifacts) + != FileVisibility.NOT_FOUND) { + // Because `getDexoptNeeded` doesn't check Pre-reboot artifacts, we + // do a simple check here to handle job resuming. If the Pre-reboot + // artifacts exist, we assume they are up-to-date because + // `PreRebootDexoptJob` would otherwise clean them up, so we skip + // this dex file. The profile and the dex file may have been changed + // since the last cancelled job run, but we don't handle such cases + // because we are supposed to dexopt every dex file only once for + // each ISA. + extendedStatusFlags |= + DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST; + continue; + } + } + GetDexoptNeededResult getDexoptNeededResult = getDexoptNeeded(target, options); @@ -639,6 +668,11 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { protected abstract boolean isDexFilePublic(@NonNull DexInfoType dexInfo); /** + * Returns true if the dex file is found. + */ + protected abstract boolean isDexFileFound(@NonNull DexInfoType dexInfo); + + /** * Returns a list of external profiles (e.g., a DM profile) that the reference profile can be * initialized from, in the order of preference. */ diff --git a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java index c890198d96..0c9350f773 100644 --- a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java +++ b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java @@ -25,8 +25,12 @@ import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; +import android.os.Binder; import android.os.Build; import android.os.CancellationSignal; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.ServiceSpecificException; import android.os.SystemProperties; import android.provider.DeviceConfig; @@ -37,9 +41,11 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.ArtServiceJobInterface; import com.android.server.art.prereboot.PreRebootDriver; +import com.android.server.art.prereboot.PreRebootStatsReporter; import java.time.Duration; import java.util.Objects; +import java.util.UUID; import java.util.concurrent.CompletableFuture; /** @@ -61,22 +67,22 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { @NonNull private final Injector mInjector; - // Job state variables. + // Job state variables. The monitor of `this` is notified when `mRunningJob` is changed. @GuardedBy("this") @Nullable private CompletableFuture<Void> mRunningJob = null; @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null; + /** Whether `mRunningJob` is running from the job scheduler's perspective. */ + @GuardedBy("this") private boolean mIsRunningJobKnownByJobScheduler = false; + /** The slot that contains the OTA update, "_a" or "_b", or null for a Mainline update. */ @GuardedBy("this") @Nullable private String mOtaSlot = null; - /** - * Whether the job has started at least once, meaning the device is expected to have staged - * files, no matter it succeed, failed, or cancelled. - * - * Note that this flag is not persisted across system server restarts. It's possible that the - * value is lost due to a system server restart caused by a crash, but this is a minor case, so - * we don't handle it here for simplicity. - */ - @GuardedBy("this") private boolean mHasStarted = false; + /** Whether to map/unmap snapshots. Only applicable to an OTA update. */ + @GuardedBy("this") private boolean mMapSnapshotsForOta = false; + + // Mutations to the global state of Pre-reboot Dexopt, including mounts, staged files, and + // stats, should only be done when there is no job running and the `this` lock is held, or by + // the job itself. public PreRebootDexoptJob(@NonNull Context context) { this(new Injector(context)); @@ -88,19 +94,27 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { } @Override - public boolean onStartJob( + public synchronized boolean onStartJob( @NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) { + JobInfo pendingJob = mInjector.getJobScheduler().getPendingJob(JOB_ID); + if (pendingJob == null + || !params.getExtras().getString("ticket").equals( + pendingJob.getExtras().getString("ticket"))) { + // Job expired. We can only get here due to a race, and this should be very rare. + return false; + } + + mIsRunningJobKnownByJobScheduler = true; // No need to handle exceptions thrown by the future because exceptions are handled inside // the future itself. - var unused = start().thenRunAsync(() -> { - try { + startLocked(() -> { + if (mIsRunningJobKnownByJobScheduler) { + mIsRunningJobKnownByJobScheduler = false; // If it failed, it means something went wrong, so we don't reschedule the job // because it will likely fail again. If it's cancelled, the job will be rescheduled // because the return value of `onStopJob` will be respected, and this call will be - // ignored. + // skipped. jobService.jobFinished(params, false /* wantsReschedule */); - } catch (RuntimeException e) { - AsLog.wtf("Unexpected exception", e); } }); // "true" means the job will continue running until `jobFinished` is called. @@ -108,36 +122,88 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { } @Override - public boolean onStopJob(@NonNull JobParameters params) { - cancel(false /* blocking */); + public synchronized boolean onStopJob(@NonNull JobParameters params) { + if (mIsRunningJobKnownByJobScheduler) { + cancelGivenLocked(mRunningJob, false /* expectInterrupt */); + mIsRunningJobKnownByJobScheduler = false; + } // "true" means to execute again with the default retry policy. return true; } - public @ScheduleStatus int schedule() { - if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { - throw new IllegalStateException("This job cannot be scheduled"); + /** + * Notifies this class that an update (OTA or Mainline) is ready. + * + * @param otaSlot The slot that contains the OTA update, "_a" or "_b", or null for a Mainline + * update. + */ + public synchronized @ScheduleStatus int onUpdateReady(@Nullable String otaSlot) { + cancelAnyLocked(); + resetLocked(); + updateOtaSlotLocked(otaSlot); + mMapSnapshotsForOta = true; + return scheduleLocked(); + } + + /** + * Same as above, but starts the job immediately, instead of going through the job scheduler. + * + * @return The future of the job, or null if Pre-reboot Dexopt is not enabled. + */ + @Nullable + public synchronized CompletableFuture<Void> onUpdateReadyStartNow(@Nullable String otaSlot) { + cancelAnyLocked(); + resetLocked(); + updateOtaSlotLocked(otaSlot); + // Don't map snapshots when running synchronously. `update_engine` maps snapshots for us. + mMapSnapshotsForOta = false; + if (!isEnabled()) { + return null; } + mInjector.getStatsReporter().recordJobScheduled(false /* isAsync */, isOtaUpdate()); + return startLocked(null /* onJobFinishedLocked */); + } - if (!SystemProperties.getBoolean("dalvik.vm.enable_pr_dexopt", false /* def */) - && !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_RUNTIME, "enable_pr_dexopt", - false /* defaultValue */)) { - AsLog.i("Pre-reboot Dexopt Job is not enabled by system property"); - return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; + public synchronized void test() { + cancelAnyLocked(); + mInjector.getPreRebootDriver().test(); + } + + /** @see #cancelGivenLocked */ + public synchronized void cancelGiven( + @NonNull CompletableFuture<Void> job, boolean expectInterrupt) { + cancelGivenLocked(job, expectInterrupt); + } + + @VisibleForTesting + public synchronized void waitForRunningJob() { + while (mRunningJob != null) { + try { + this.wait(); + } catch (InterruptedException e) { + AsLog.wtf("Interrupted", e); + } } + } - // If `pm.dexopt.disable_bg_dexopt` is set, the user probably means to disable any dexopt - // jobs in the background. - if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) { - AsLog.i("Pre-reboot Dexopt Job is disabled by system property " - + "'pm.dexopt.disable_bg_dexopt'"); + @GuardedBy("this") + private @ScheduleStatus int scheduleLocked() { + if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { + throw new IllegalStateException("This job cannot be scheduled"); + } + + if (!isEnabled()) { return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; } + String ticket = UUID.randomUUID().toString(); + PersistableBundle extras = new PersistableBundle(1 /* capacity */); + extras.putString("ticket", ticket); JobInfo info = new JobInfo .Builder(JOB_ID, new ComponentName(JOB_PKG_NAME, BackgroundDexoptJobService.class.getName())) + .setExtras(extras) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setRequiresBatteryNotLow(true) @@ -145,9 +211,19 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { .setMinimumLatency(Duration.ofMinutes(10).toMillis()) .build(); - /* @JobScheduler.Result */ int result = mInjector.getJobScheduler().schedule(info); + /* @JobScheduler.Result */ int result; + + // This operation requires the uid to be "system" (1000). + long identityToken = Binder.clearCallingIdentity(); + try { + result = mInjector.getJobScheduler().schedule(info); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (result == JobScheduler.RESULT_SUCCESS) { AsLog.i("Pre-reboot Dexopt Job scheduled"); + mInjector.getStatsReporter().recordJobScheduled(true /* isAsync */, isOtaUpdate()); return ArtFlags.SCHEDULE_SUCCESS; } else { AsLog.i("Failed to schedule Pre-reboot Dexopt Job"); @@ -155,62 +231,112 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { } } - public void unschedule() { + @GuardedBy("this") + private void unscheduleLocked() { if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { throw new IllegalStateException("This job cannot be unscheduled"); } - mInjector.getJobScheduler().cancel(JOB_ID); + // This operation requires the uid to be "system" (1000). + long identityToken = Binder.clearCallingIdentity(); + try { + mInjector.getJobScheduler().cancel(JOB_ID); + } finally { + Binder.restoreCallingIdentity(identityToken); + } } + /** + * The future returns true if the job is cancelled by the job scheduler. + * + * Can only be called when there is no running job. + */ + @GuardedBy("this") @NonNull - public synchronized CompletableFuture<Void> start() { - if (mRunningJob != null) { - AsLog.i("Job is already running"); - return mRunningJob; - } + private CompletableFuture<Void> startLocked(@Nullable Runnable onJobFinishedLocked) { + Utils.check(mRunningJob == null); String otaSlot = mOtaSlot; + boolean mapSnapshotsForOta = mMapSnapshotsForOta; var cancellationSignal = mCancellationSignal = new CancellationSignal(); - mHasStarted = true; mRunningJob = new CompletableFuture().runAsync(() -> { + markHasStarted(true); try { - // TODO(b/336239721): Consume the result and report metrics. - mInjector.getPreRebootDriver().run(otaSlot, cancellationSignal); + mInjector.getPreRebootDriver().run(otaSlot, mapSnapshotsForOta, cancellationSignal); } catch (RuntimeException e) { AsLog.e("Fatal error", e); } finally { synchronized (this) { + if (onJobFinishedLocked != null) { + try { + onJobFinishedLocked.run(); + } catch (RuntimeException e) { + AsLog.wtf("Unexpected exception", e); + } + } mRunningJob = null; mCancellationSignal = null; + this.notifyAll(); } } }); + this.notifyAll(); return mRunningJob; } /** - * Cancels the job. + * Cancels the given job and waits for it to exit, if it's running. Temporarily releases the + * lock when waiting for the job to exit. * - * @param blocking whether to wait for the job to exit. + * When this method exits, it's guaranteed that the given job is not running, but another job + * might be running. + * + * @param expectInterrupt if true, this method returns immediately when the thread is + * interrupted, with no guarantee on the job state */ - public void cancel(boolean blocking) { - CompletableFuture<Void> runningJob = null; - synchronized (this) { - if (mRunningJob == null) { - return; + @GuardedBy("this") + private void cancelGivenLocked(@NonNull CompletableFuture<Void> job, boolean expectInterrupt) { + while (mRunningJob == job) { + if (!mCancellationSignal.isCanceled()) { + mCancellationSignal.cancel(); + AsLog.i("Job cancelled"); + } + try { + this.wait(); + } catch (InterruptedException e) { + if (expectInterrupt) { + return; + } + AsLog.wtf("Interrupted", e); } - - mCancellationSignal.cancel(); - AsLog.i("Job cancelled"); - runningJob = mRunningJob; } - if (blocking) { - Utils.getFuture(runningJob); + } + + /** + * Cancels any running job, prevents the pending job (if any) from being started by the job + * scheduler, and waits for the running job to exit. Temporarily releases the lock when waiting + * for the job to exit. + * + * When this method exits, it's guaranteed that no job is running. + */ + @GuardedBy("this") + private void cancelAnyLocked() { + unscheduleLocked(); + while (mRunningJob != null) { + if (!mCancellationSignal.isCanceled()) { + mCancellationSignal.cancel(); + AsLog.i("Job cancelled"); + } + try { + this.wait(); + } catch (InterruptedException e) { + AsLog.wtf("Interrupted", e); + } } } - public synchronized void updateOtaSlot(@NonNull String value) { + @GuardedBy("this") + private void updateOtaSlotLocked(@Nullable String value) { Utils.check(value == null || value.equals("_a") || value.equals("_b")); // It's not possible that this method is called with two different slots. Utils.check(mOtaSlot == null || value == null || Objects.equals(mOtaSlot, value)); @@ -222,8 +348,66 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { } } - public synchronized boolean hasStarted() { - return mHasStarted; + private boolean isEnabled() { + boolean syspropEnable = + SystemProperties.getBoolean("dalvik.vm.enable_pr_dexopt", false /* def */); + boolean deviceConfigEnable = mInjector.getDeviceConfigBoolean( + DeviceConfig.NAMESPACE_RUNTIME, "enable_pr_dexopt", false /* defaultValue */); + boolean deviceConfigForceDisable = + mInjector.getDeviceConfigBoolean(DeviceConfig.NAMESPACE_RUNTIME, + "force_disable_pr_dexopt", false /* defaultValue */); + if ((!syspropEnable && !deviceConfigEnable) || deviceConfigForceDisable) { + AsLog.i(String.format( + "Pre-reboot Dexopt Job is not enabled (sysprop:dalvik.vm.enable_pr_dexopt=%b, " + + "device_config:enable_pr_dexopt=%b, " + + "device_config:force_disable_pr_dexopt=%b)", + syspropEnable, deviceConfigEnable, deviceConfigForceDisable)); + return false; + } + // If `pm.dexopt.disable_bg_dexopt` is set, the user probably means to disable any dexopt + // jobs in the background. + if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) { + AsLog.i("Pre-reboot Dexopt Job is disabled by system property " + + "'pm.dexopt.disable_bg_dexopt'"); + return false; + } + return true; + } + + public boolean isAsyncForOta() { + return SystemProperties.getBoolean("dalvik.vm.pr_dexopt_async_for_ota", false /* def */); + } + + @GuardedBy("this") + private void resetLocked() { + mInjector.getStatsReporter().delete(); + if (hasStarted()) { + try { + mInjector.getArtd().cleanUpPreRebootStagedFiles(); + } catch (ServiceSpecificException | RemoteException e) { + AsLog.e("Failed to clean up obsolete Pre-reboot staged files", e); + } + markHasStarted(false); + } + } + + /** + * Whether the job has started at least once, meaning the device is expected to have staged + * files, no matter it succeed, failed, or cancelled. + * + * This flag is survives across system server restarts, but not device reboots. + */ + public boolean hasStarted() { + return SystemProperties.getBoolean("dalvik.vm.pre-reboot.has-started", false /* def */); + } + + private void markHasStarted(boolean value) { + ArtJni.setProperty("dalvik.vm.pre-reboot.has-started", String.valueOf(value)); + } + + @GuardedBy("this") + private boolean isOtaUpdate() { + return mOtaSlot != null; } /** @@ -248,5 +432,23 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { public PreRebootDriver getPreRebootDriver() { return new PreRebootDriver(mContext); } + + @NonNull + public PreRebootStatsReporter getStatsReporter() { + return new PreRebootStatsReporter(); + } + + @NonNull + public IArtd getArtd() { + return ArtdRefCache.getInstance().getArtd(); + } + + // Wrap `DeviceConfig` to avoid mocking it directly in tests. `DeviceConfig` backs + // read-write Trunk Stable flags used by the framework. + @NonNull + public boolean getDeviceConfigBoolean( + @NonNull String namespace, @NonNull String name, boolean defaultValue) { + return DeviceConfig.getBoolean(namespace, name, defaultValue); + } } } diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexopter.java b/libartservice/service/java/com/android/server/art/PrimaryDexopter.java index e4475d8503..f8234c4df9 100644 --- a/libartservice/service/java/com/android/server/art/PrimaryDexopter.java +++ b/libartservice/service/java/com/android/server/art/PrimaryDexopter.java @@ -118,6 +118,17 @@ public class PrimaryDexopter extends Dexopter<DetailedPrimaryDexInfo> { } @Override + protected boolean isDexFileFound(@NonNull DetailedPrimaryDexInfo dexInfo) { + try { + return mInjector.getArtd().getDexFileVisibility(dexInfo.dexPath()) + != FileVisibility.NOT_FOUND; + } catch (ServiceSpecificException | RemoteException e) { + AsLog.e("Failed to get visibility of " + dexInfo.dexPath(), e); + return false; + } + } + + @Override @NonNull protected List<ProfilePath> getExternalProfiles(@NonNull DetailedPrimaryDexInfo dexInfo) { return PrimaryDexUtils.getExternalProfiles(dexInfo); diff --git a/libartservice/service/java/com/android/server/art/SecondaryDexopter.java b/libartservice/service/java/com/android/server/art/SecondaryDexopter.java index 07c6c0d654..57fe174d6a 100644 --- a/libartservice/service/java/com/android/server/art/SecondaryDexopter.java +++ b/libartservice/service/java/com/android/server/art/SecondaryDexopter.java @@ -87,6 +87,12 @@ public class SecondaryDexopter extends Dexopter<CheckedSecondaryDexInfo> { } @Override + protected boolean isDexFileFound(@NonNull CheckedSecondaryDexInfo dexInfo) { + // `getDexInfoList` has already excluded non-existing dex files. + return true; + } + + @Override @NonNull protected List<ProfilePath> getExternalProfiles(@NonNull CheckedSecondaryDexInfo dexInfo) { // A secondary dex file doesn't have any external profile to use. diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java index 8132dbbab2..31e2fec4c0 100644 --- a/libartservice/service/java/com/android/server/art/Utils.java +++ b/libartservice/service/java/com/android/server/art/Utils.java @@ -279,16 +279,24 @@ public final class Utils { try { return future.get(); } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } - throw new RuntimeException(cause); + throw toRuntimeException(e.getCause()); } catch (InterruptedException e) { throw new RuntimeException(e); } } + @NonNull + public static RuntimeException toRuntimeException(@NonNull Throwable t) { + if (t instanceof RuntimeException r) { + return r; + } + var r = new RuntimeException(t); + // Clear the unhelpful stack trace, which is the stack trace of the constructor call above, + // so that the user can focus on the stack trace of `t`. + r.setStackTrace(new StackTraceElement[0]); + return r; + } + /** * Returns true if the given package is dexoptable. * diff --git a/libartservice/service/java/com/android/server/art/model/DexoptResult.java b/libartservice/service/java/com/android/server/art/model/DexoptResult.java index 8dd75a119e..16bc282d55 100644 --- a/libartservice/service/java/com/android/server/art/model/DexoptResult.java +++ b/libartservice/service/java/com/android/server/art/model/DexoptResult.java @@ -75,6 +75,13 @@ public abstract class DexoptResult { */ public static final int EXTENDED_SKIPPED_NO_DEX_CODE = 1 << 1; /** + * Dexopt is skipped because this operation is for Pre-reboot and the Pre-reboot artifacts + * already exist. + * + * @hide + */ + public static final int EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST = 1 << 3; + /** * Dexopt encountered errors when processing the profiles that are external to the device, * including the profile in the DM file and the profile embedded in the dex container file. * Details of the errors can be found in {@link @@ -90,6 +97,7 @@ public abstract class DexoptResult { @IntDef(flag = true, prefix = {"EXTENDED_"}, value = { EXTENDED_SKIPPED_STORAGE_LOW, EXTENDED_SKIPPED_NO_DEX_CODE, + EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST, EXTENDED_BAD_EXTERNAL_PROFILE, }) // clang-format on @@ -180,6 +188,9 @@ public abstract class DexoptResult { if ((flags & DexoptResult.EXTENDED_SKIPPED_NO_DEX_CODE) != 0) { strs.add("EXTENDED_SKIPPED_NO_DEX_CODE"); } + if ((flags & DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST) != 0) { + strs.add("EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST"); + } if ((flags & DexoptResult.EXTENDED_BAD_EXTERNAL_PROFILE) != 0) { strs.add("EXTENDED_BAD_EXTERNAL_PROFILE"); } diff --git a/libartservice/service/java/com/android/server/art/model/OperationProgress.java b/libartservice/service/java/com/android/server/art/model/OperationProgress.java index a0a7ddc4fc..90186726ea 100644 --- a/libartservice/service/java/com/android/server/art/model/OperationProgress.java +++ b/libartservice/service/java/com/android/server/art/model/OperationProgress.java @@ -16,14 +16,21 @@ package com.android.server.art.model; +import static com.android.server.art.model.DexoptResult.PackageDexoptResult; + import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemApi; import com.android.internal.annotations.Immutable; import com.google.auto.value.AutoValue; -/** @hide */ +/** + * Represents the progress of an operation. Currently, this is only for batch dexopt. + * + * @hide + */ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) @Immutable @AutoValue @@ -32,8 +39,9 @@ public abstract class OperationProgress { protected OperationProgress() {} /** @hide */ - public static @NonNull OperationProgress create(int current, int total) { - return new AutoValue_OperationProgress(current, total); + public static @NonNull OperationProgress create( + int current, int total, @Nullable PackageDexoptResult packageDexoptResult) { + return new AutoValue_OperationProgress(current, total, packageDexoptResult); } /** The overall progress, in the range of [0, 100]. */ @@ -59,4 +67,12 @@ public abstract class OperationProgress { * @hide */ public abstract int getTotal(); + + /** + * The last package dexopt result, or null if {@link #getCurrent} returns 0. Only applicable if + * this class represents batch dexopt progress. + * + * @hide + */ + public abstract @Nullable PackageDexoptResult getLastPackageDexoptResult(); } diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java index 44809b1c50..a30ca4ab74 100644 --- a/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java @@ -17,6 +17,8 @@ package com.android.server.art.prereboot; import static com.android.server.art.IDexoptChrootSetup.CHROOT_DIR; +import static com.android.server.art.prereboot.PreRebootManagerInterface.SystemRequirementException; +import static com.android.server.art.proto.PreRebootStats.Status; import android.annotation.NonNull; import android.annotation.Nullable; @@ -26,22 +28,39 @@ import android.os.Build; import android.os.CancellationSignal; import android.os.RemoteException; import android.os.ServiceSpecificException; +import android.system.ErrnoException; +import android.system.Os; import androidx.annotation.RequiresApi; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.art.ArtJni; import com.android.server.art.ArtManagerLocal; import com.android.server.art.ArtModuleServiceInitializer; +import com.android.server.art.ArtdRefCache; import com.android.server.art.AsLog; import com.android.server.art.GlobalInjector; +import com.android.server.art.IArtd; import com.android.server.art.IDexoptChrootSetup; +import com.android.server.art.PreRebootDexoptJob; import com.android.server.art.Utils; import dalvik.system.DelegateLastClassLoader; +import libcore.io.Streams; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + /** * Drives Pre-reboot Dexopt, through reflection. * + * DO NOT use this class directly. Use {@link PreRebootDexoptJob}. + * * During Pre-reboot Dexopt, the old version of this code is run. * * @hide @@ -60,69 +79,128 @@ public class PreRebootDriver { } /** - * Runs Pre-reboot Dexopt and returns whether it is successful. + * Runs Pre-reboot Dexopt and returns whether it is successful. Returns false if Pre-reboot + * dexopt failed, the system requirement check failed, or system requirements are not met. * * @param otaSlot The slot that contains the OTA update, "_a" or "_b", or null for a Mainline * update. + * @param mapSnapshotsForOta Whether to map/unmap snapshots. Only applicable to an OTA update. */ - public boolean run(@Nullable String otaSlot, @NonNull CancellationSignal cancellationSignal) { + public boolean run(@Nullable String otaSlot, boolean mapSnapshotsForOta, + @NonNull CancellationSignal cancellationSignal) { + var statsReporter = new PreRebootStatsReporter(); + boolean success = false; + boolean systemRequirementCheckFailed = false; try { - setUp(otaSlot); + statsReporter.recordJobStarted(); + setUp(otaSlot, mapSnapshotsForOta); runFromChroot(cancellationSignal); + success = true; return true; } catch (RemoteException e) { Utils.logArtdException(e); } catch (ServiceSpecificException e) { AsLog.e("Failed to set up chroot", e); + } catch (SystemRequirementException e) { + systemRequirementCheckFailed = true; + AsLog.e("System requirement check failed", e); } catch (ReflectiveOperationException e) { - AsLog.e("Failed to run pre-reboot dexopt", e); + Throwable cause = e.getCause(); + if (cause != null + && cause.getClass().getName().equals( + SystemRequirementException.class.getName())) { + // For future use only. Can't happen for now. + systemRequirementCheckFailed = true; + AsLog.e("System requirement check failed in chroot", cause); + } else { + AsLog.wtf("Failed to run Pre-reboot Dexopt", e); + } + } catch (IOException | ErrnoException e) { + AsLog.e("Failed to run Pre-reboot Dexopt", e); } finally { - tearDown(); - } - return false; - } - - private void setUp(@Nullable String otaSlot) throws RemoteException { - mInjector.getDexoptChrootSetup().setUp(otaSlot); - } - - private void tearDown() { - // In general, the teardown unmounts apexes and partitions, and open files can keep the - // mounts busy so that they cannot be unmounted. Therefore, two things can prevent the - // teardown from succeeding: a running Pre-reboot artd process and the new `service-art.jar` - // opened and mapped by system server. They are managed by the service manager and the - // runtime respectively. There aren't reliable APIs to kill the former or close the latter, - // so we have to do them by triggering GC and finalization, with sleep and retry mechanism. - for (int numRetries = 3; numRetries > 0;) { try { - Runtime.getRuntime().gc(); - Runtime.getRuntime().runFinalization(); - // Wait for the service manager to shut down artd. The shutdown is asynchronous. - Utils.sleep(5000); - mInjector.getDexoptChrootSetup().tearDown(); - return; + // No need to pass `mapSnapshotsForOta` because `setUp` stores this information in a + // temp file. + tearDown(); } catch (RemoteException e) { Utils.logArtdException(e); - } catch (ServiceSpecificException e) { + } catch (ServiceSpecificException | IOException e) { AsLog.e("Failed to tear down chroot", e); - } catch (IllegalStateException e) { - // Not expected, but we still want retries in such an extreme case. - AsLog.wtf("Unexpected exception", e); + } finally { + statsReporter.recordJobEnded(success, systemRequirementCheckFailed); } + } + return false; + } - if (--numRetries > 0) { - AsLog.i("Retrying...."); - Utils.sleep(30000); + public void test() { + boolean teardownAttempted = false; + try { + setUp(null /* otaSlot */, false /* mapSnapshotsForOta */); + // Ideally, we should try dexopting some packages here. However, it's not trivial to + // pass a package list into chroot. Besides, we need to generate boot images even if we + // dexopt only one package, and that can easily make the test fail the CTS quality + // requirement on test duration (<30s). + teardownAttempted = true; + tearDown(); + } catch (SystemRequirementException e) { + throw new AssertionError("System requirement check failed", e); + } catch (RemoteException | IOException e) { + throw new AssertionError("Unexpected exception", e); + } finally { + if (!teardownAttempted) { + try { + tearDown(); + } catch (RemoteException | IOException | RuntimeException e) { + // Do nothing. + } } } } + private void setUp(@Nullable String otaSlot, boolean mapSnapshotsForOta) + throws RemoteException, SystemRequirementException { + mInjector.getDexoptChrootSetup().setUp(otaSlot, mapSnapshotsForOta); + if (!mInjector.getArtd().checkPreRebootSystemRequirements(CHROOT_DIR)) { + throw new SystemRequirementException("See logs for details"); + } + mInjector.getDexoptChrootSetup().init(); + } + + private void tearDown() throws RemoteException, IOException { + // In general, the teardown unmounts apexes and partitions, and open files can keep the + // mounts busy so that they cannot be unmounted. Therefore, a running Pre-reboot artd + // process can prevent the teardown from succeeding. It's managed by the service manager, + // and there isn't a reliable API to kill it. We deal with it in two steps: + // 1. Trigger GC and finalization. The service manager should gracefully shut it down, since + // there is no reference to it as this point. + // 2. Call `ensureNoProcessInDir` to wait for it to exit. If it doesn't exit in 5 seconds, + // `ensureNoProcessInDir` will then kill it. + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + // At this point, no process other than `artd` is expected to be running. `runFromChroot` + // blocks on `artd` calls, even upon cancellation, and `artd` in turn waits for child + // processes to exit, even if they are killed due to the cancellation. + ArtJni.ensureNoProcessInDir(CHROOT_DIR, 5000 /* timeoutMs */); + mInjector.getDexoptChrootSetup().tearDown(); + } + private void runFromChroot(@NonNull CancellationSignal cancellationSignal) - throws ReflectiveOperationException { + throws ReflectiveOperationException, IOException, ErrnoException { String chrootArtDir = CHROOT_DIR + "/apex/com.android.art"; String dexPath = chrootArtDir + "/javalib/service-art.jar"; - var classLoader = - new DelegateLastClassLoader(dexPath, this.getClass().getClassLoader() /* parent */); + + // We load the dex file into the memory and close it. In this way, the classloader won't + // prevent unmounting even if it fails to unload. + ClassLoader classLoader; + FileDescriptor memfd = Os.memfd_create("in memory from " + dexPath, 0 /* flags */); + try (FileOutputStream out = new FileOutputStream(memfd); + InputStream in = new FileInputStream(dexPath)) { + Streams.copy(in, out); + classLoader = new DelegateLastClassLoader("/proc/self/fd/" + memfd.getInt$(), + this.getClass().getClassLoader() /* parent */); + } + Class<?> preRebootManagerClass = classLoader.loadClass("com.android.server.art.prereboot.PreRebootManager"); // Check if the dex file is loaded successfully. Note that the constructor of @@ -160,5 +238,10 @@ public class PreRebootDriver { public IDexoptChrootSetup getDexoptChrootSetup() { return GlobalInjector.getInstance().getDexoptChrootSetup(); } + + @NonNull + public IArtd getArtd() { + return ArtdRefCache.getInstance().getArtd(); + } } } diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java index 6d808ae12c..84c2b34a02 100644 --- a/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java @@ -21,14 +21,17 @@ import android.annotation.Nullable; import android.content.Context; import android.os.ArtModuleServiceManager; import android.os.Build; +import android.os.CancellationSignal; import android.os.RemoteException; import androidx.annotation.RequiresApi; import com.android.server.art.ArtdRefCache; +import com.android.server.art.AsLog; import com.android.server.art.DexUseManagerLocal; import com.android.server.art.GlobalInjector; import com.android.server.art.IArtd; +import com.android.server.art.IArtdCancellationSignal; import com.android.server.art.IDexoptChrootSetup; import java.util.Objects; @@ -50,15 +53,19 @@ public class PreRebootGlobalInjector extends GlobalInjector { mArtModuleServiceManager = artModuleServiceManager; } - public static void init( - @NonNull ArtModuleServiceManager artModuleServiceManager, @NonNull Context context) { + /** Returns true on success, or false on cancellation. Throws on failure. */ + public static boolean init(@NonNull ArtModuleServiceManager artModuleServiceManager, + @NonNull Context context, @NonNull CancellationSignal cancellationSignal) { var instance = new PreRebootGlobalInjector(artModuleServiceManager); GlobalInjector.setInstance(instance); + // This call throws if artd cannot be initialized. + if (!instance.initArtd(cancellationSignal)) { + return false; + } try (var pin = ArtdRefCache.getInstance().new Pin()) { - // Fail early if artd cannot be initialized. - ArtdRefCache.getInstance().getArtd(); instance.mDexUseManager = DexUseManagerLocal.createInstance(context); } + return true; } @Override @@ -69,16 +76,45 @@ public class PreRebootGlobalInjector extends GlobalInjector { @Override public void checkArtModuleServiceManager() {} - @Override @NonNull - public IArtd getArtd() { + private IArtd getArtdNoInit() { IArtd artd = IArtd.Stub.asInterface( mArtModuleServiceManager.getArtdPreRebootServiceRegisterer().waitForService()); if (artd == null) { throw new IllegalStateException("Unable to connect to artd for pre-reboot dexopt"); } + return artd; + } + + private boolean initArtd(@NonNull CancellationSignal cancellationSignal) { + if (cancellationSignal.isCanceled()) { + return false; + } + try { + IArtd artd = getArtdNoInit(); + IArtdCancellationSignal artdCancellationSignal = artd.createCancellationSignal(); + cancellationSignal.setOnCancelListener(() -> { + try { + artdCancellationSignal.cancel(); + } catch (RemoteException e) { + AsLog.e("An error occurred when sending a cancellation signal", e); + } + }); + return artd.preRebootInit(artdCancellationSignal); + } catch (RemoteException e) { + throw new IllegalStateException("Unable to initialize artd for pre-reboot dexopt", e); + } finally { + // Make sure artd does not leak even if the caller holds `cancellationSignal` forever. + cancellationSignal.setOnCancelListener(null); + } + } + + @Override + @NonNull + public IArtd getArtd() { + IArtd artd = getArtdNoInit(); try { - artd.preRebootInit(); + artd.preRebootInit(null /* cancellationSignal */); } catch (RemoteException e) { throw new IllegalStateException("Unable to initialize artd for pre-reboot dexopt", e); } diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java index d6947ba156..d4c4e570e2 100644 --- a/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java @@ -16,6 +16,9 @@ package com.android.server.art.prereboot; +import static com.android.server.art.model.DexoptResult.PackageDexoptResult; +import static com.android.server.art.proto.PreRebootStats.Status; + import android.annotation.NonNull; import android.content.Context; import android.os.ArtModuleServiceManager; @@ -27,10 +30,22 @@ import androidx.annotation.RequiresApi; import com.android.server.LocalManagerRegistry; import com.android.server.art.ArtManagerLocal; import com.android.server.art.ArtdRefCache; +import com.android.server.art.AsLog; import com.android.server.art.ReasonMapping; +import com.android.server.art.Utils; +import com.android.server.art.model.ArtFlags; +import com.android.server.art.model.DexoptResult; +import com.android.server.art.model.OperationProgress; import com.android.server.pm.PackageManagerLocal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; /** * Implementation of {@link PreRebootManagerInterface}. @@ -46,18 +61,75 @@ import java.util.Objects; public class PreRebootManager implements PreRebootManagerInterface { public void run(@NonNull ArtModuleServiceManager artModuleServiceManager, @NonNull Context context, @NonNull CancellationSignal cancellationSignal) { + ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(); try { - PreRebootGlobalInjector.init(artModuleServiceManager, context); + if (!PreRebootGlobalInjector.init( + artModuleServiceManager, context, cancellationSignal)) { + return; + } ArtManagerLocal artManagerLocal = new ArtManagerLocal(context); PackageManagerLocal packageManagerLocal = Objects.requireNonNull( LocalManagerRegistry.getManager(PackageManagerLocal.class)); + + var progressSession = new PreRebootStatsReporter().new ProgressSession(); + + // Contains four values: skipped, performed, failed, has pre-reboot artifacts. + List<Integer> values = new ArrayList(List.of(0, 0, 0, 0)); + + // Record every progress change right away, in case the job is interrupted by a reboot. + Consumer<OperationProgress> progressCallback = progress -> { + PackageDexoptResult result = progress.getLastPackageDexoptResult(); + if (result == null) { + return; + } + switch (result.getStatus()) { + case DexoptResult.DEXOPT_SKIPPED: + if (hasExistingArtifacts(result)) { + values.set(1, values.get(1) + 1); + } else { + values.set(0, values.get(0) + 1); + } + break; + case DexoptResult.DEXOPT_PERFORMED: + values.set(1, values.get(1) + 1); + break; + case DexoptResult.DEXOPT_FAILED: + values.set(2, values.get(2) + 1); + break; + case DexoptResult.DEXOPT_CANCELLED: + break; + default: + throw new IllegalStateException("Unknown status: " + result.getStatus()); + } + if (hasExistingArtifacts(result) || result.hasUpdatedArtifacts()) { + values.set(3, values.get(3) + 1); + } + + progressSession.recordProgress(values.get(0), values.get(1), values.get(2), + progress.getTotal(), values.get(3)); + }; + try (var snapshot = packageManagerLocal.withFilteredSnapshot()) { artManagerLocal.dexoptPackages(snapshot, ReasonMapping.REASON_PRE_REBOOT_DEXOPT, - cancellationSignal, null /* processCallbackExecutor */, - null /* processCallback */); + cancellationSignal, callbackExecutor, + Map.of(ArtFlags.PASS_MAIN, progressCallback)); } } finally { ArtdRefCache.getInstance().reset(); + callbackExecutor.shutdown(); + try { + // Make sure we have no running threads when we tear down. + callbackExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + AsLog.wtf("Interrupted", e); + } } } + + private boolean hasExistingArtifacts(@NonNull PackageDexoptResult result) { + return result.getDexContainerFileDexoptResults().stream().anyMatch(fileResult + -> (fileResult.getExtendedStatusFlags() + & DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST) + != 0); + } } diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java index 8422debdbc..85f8b3f931 100644 --- a/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java @@ -42,5 +42,11 @@ import androidx.annotation.RequiresApi; @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) public interface PreRebootManagerInterface { void run(@NonNull ArtModuleServiceManager artModuleServiceManager, @NonNull Context context, - @NonNull CancellationSignal cancellationSignal); + @NonNull CancellationSignal cancellationSignal) throws SystemRequirementException; + + public static class SystemRequirementException extends Exception { + public SystemRequirementException(@NonNull String message) { + super(message); + } + } } diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootStatsReporter.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootStatsReporter.java new file mode 100644 index 0000000000..b14965df21 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootStatsReporter.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.art.prereboot; + +import static com.android.server.art.proto.PreRebootStats.JobRun; +import static com.android.server.art.proto.PreRebootStats.JobType; +import static com.android.server.art.proto.PreRebootStats.Status; + +import android.annotation.NonNull; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalManagerRegistry; +import com.android.server.art.ArtManagerLocal; +import com.android.server.art.ArtStatsLog; +import com.android.server.art.ArtdRefCache; +import com.android.server.art.AsLog; +import com.android.server.art.ReasonMapping; +import com.android.server.art.Utils; +import com.android.server.art.model.DexoptStatus; +import com.android.server.art.proto.PreRebootStats; +import com.android.server.pm.PackageManagerLocal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * A helper class to report the Pre-reboot Dexopt metrics to StatsD. + * + * This class is not thread-safe. + * + * During Pre-reboot Dexopt, both the old version and the new version of this code is run. The old + * version writes to disk first, and the new version writes to disk later. After reboot, the new + * version loads from disk. + * + * @hide + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public class PreRebootStatsReporter { + private static final String FILENAME = "/data/system/pre-reboot-stats.pb"; + + @NonNull private final Injector mInjector; + + public PreRebootStatsReporter() { + this(new Injector()); + } + + /** @hide */ + @VisibleForTesting + public PreRebootStatsReporter(@NonNull Injector injector) { + mInjector = injector; + } + + public void recordJobScheduled(boolean isAsync, boolean isOtaUpdate) { + PreRebootStats.Builder statsBuilder = PreRebootStats.newBuilder(); + statsBuilder.setStatus(Status.STATUS_SCHEDULED) + .setJobType(isOtaUpdate ? JobType.JOB_TYPE_OTA : JobType.JOB_TYPE_MAINLINE); + // Omit job_scheduled_timestamp_millis to indicate a synchronous job. + if (isAsync) { + statsBuilder.setJobScheduledTimestampMillis(mInjector.getCurrentTimeMillis()); + } + save(statsBuilder); + } + + public void recordJobStarted() { + PreRebootStats.Builder statsBuilder = load(); + if (statsBuilder.getStatus() == Status.STATUS_UNKNOWN) { + // Failed to load, the error is already logged. + return; + } + + JobRun.Builder runBuilder = + JobRun.newBuilder().setJobStartedTimestampMillis(mInjector.getCurrentTimeMillis()); + statsBuilder.setStatus(Status.STATUS_STARTED) + .addJobRuns(runBuilder) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + // Some packages may have artifacts from a previously cancelled job, but we count + // from scratch for simplicity. + .setPackagesWithArtifactsBeforeRebootCount(0); + save(statsBuilder); + } + + public class ProgressSession { + private @NonNull PreRebootStats.Builder mStatsBuilder = load(); + + public void recordProgress(int skippedPackageCount, int optimizedPackageCount, + int failedPackageCount, int totalPackageCount, + int packagesWithArtifactsBeforeRebootCount) { + if (mStatsBuilder.getStatus() == Status.STATUS_UNKNOWN) { + // Failed to load, the error is already logged. + return; + } + + mStatsBuilder.setSkippedPackageCount(skippedPackageCount) + .setOptimizedPackageCount(optimizedPackageCount) + .setFailedPackageCount(failedPackageCount) + .setTotalPackageCount(totalPackageCount) + .setPackagesWithArtifactsBeforeRebootCount( + packagesWithArtifactsBeforeRebootCount); + save(mStatsBuilder); + } + } + + public void recordJobEnded(boolean success, boolean systemRequirementCheckFailed) { + PreRebootStats.Builder statsBuilder = load(); + if (statsBuilder.getStatus() == Status.STATUS_UNKNOWN) { + // Failed to load, the error is already logged. + return; + } + + List<JobRun> jobRuns = statsBuilder.getJobRunsList(); + Utils.check(jobRuns.size() > 0); + JobRun lastRun = jobRuns.get(jobRuns.size() - 1); + Utils.check(lastRun.getJobEndedTimestampMillis() == 0); + + JobRun.Builder runBuilder = JobRun.newBuilder(lastRun).setJobEndedTimestampMillis( + mInjector.getCurrentTimeMillis()); + + Status status; + if (success) { + // The job is cancelled if it hasn't done package scanning (total package count is 0), + // or it's interrupted in the middle of package processing (package counts don't add up + // to the total). + // TODO(b/336239721): Move this logic to the server. + if (statsBuilder.getTotalPackageCount() > 0 + && (statsBuilder.getOptimizedPackageCount() + + statsBuilder.getFailedPackageCount() + + statsBuilder.getSkippedPackageCount()) + == statsBuilder.getTotalPackageCount()) { + status = Status.STATUS_FINISHED; + } else { + status = Status.STATUS_CANCELLED; + } + } else { + if (systemRequirementCheckFailed) { + status = Status.STATUS_ABORTED_SYSTEM_REQUIREMENTS; + } else { + status = Status.STATUS_FAILED; + } + } + + statsBuilder.setStatus(status).setJobRuns(jobRuns.size() - 1, runBuilder); + save(statsBuilder); + } + + public class AfterRebootSession { + private @NonNull Set<String> mPackagesWithArtifacts = new HashSet<>(); + + public void recordPackageWithArtifacts(@NonNull String packageName) { + mPackagesWithArtifacts.add(packageName); + } + + public void reportAsync() { + new CompletableFuture().runAsync(this::report).exceptionally(t -> { + AsLog.e("Failed to report stats", t); + return null; + }); + } + + @VisibleForTesting + public void report() { + PreRebootStats.Builder statsBuilder = load(); + delete(); + + if (statsBuilder.getStatus() == Status.STATUS_UNKNOWN) { + // Job not scheduled, probably because Pre-reboot Dexopt is not enabled. + return; + } + + ArtManagerLocal artManagerLocal = mInjector.getArtManagerLocal(); + + // This takes some time (~3ms per package). It probably fine because we are running + // asynchronously. Consider removing this in the future. + int packagesWithArtifactsUsableCount; + try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot(); + var pin = mInjector.createArtdPin()) { + packagesWithArtifactsUsableCount = + (int) mPackagesWithArtifacts.stream() + .map(packageName + -> artManagerLocal.getDexoptStatus(snapshot, packageName)) + .filter(status -> hasUsablePreRebootArtifacts(status)) + .count(); + } + + List<JobRun> jobRuns = statsBuilder.getJobRunsList(); + // The total duration of all runs, or -1 if any run didn't end. + long jobDurationMs = 0; + for (JobRun run : jobRuns) { + if (run.getJobEndedTimestampMillis() == 0) { + jobDurationMs = -1; + break; + } + jobDurationMs += + run.getJobEndedTimestampMillis() - run.getJobStartedTimestampMillis(); + } + if (jobRuns.size() == 0) { + jobDurationMs = -1; + } + long jobLatencyMs = + (jobRuns.size() > 0 && statsBuilder.getJobScheduledTimestampMillis() > 0) + ? (jobRuns.get(0).getJobStartedTimestampMillis() + - statsBuilder.getJobScheduledTimestampMillis()) + : -1; + + mInjector.writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED, + getStatusForStatsd(statsBuilder.getStatus()), + statsBuilder.getOptimizedPackageCount(), statsBuilder.getFailedPackageCount(), + statsBuilder.getSkippedPackageCount(), statsBuilder.getTotalPackageCount(), + jobDurationMs, jobLatencyMs, mPackagesWithArtifacts.size(), + packagesWithArtifactsUsableCount, jobRuns.size(), + statsBuilder.getPackagesWithArtifactsBeforeRebootCount(), + getJobTypeForStatsd(statsBuilder.getJobType())); + } + } + + private int getStatusForStatsd(@NonNull Status status) { + switch (status) { + case STATUS_UNKNOWN: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN; + case STATUS_SCHEDULED: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_SCHEDULED; + case STATUS_STARTED: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_STARTED; + case STATUS_FINISHED: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FINISHED; + case STATUS_FAILED: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FAILED; + case STATUS_CANCELLED: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_CANCELLED; + case STATUS_ABORTED_SYSTEM_REQUIREMENTS: + return ArtStatsLog + .PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORTED_SYSTEM_REQUIREMENTS; + default: + throw new IllegalStateException("Unknown status: " + status.getNumber()); + } + } + + private int getJobTypeForStatsd(@NonNull JobType jobType) { + switch (jobType) { + case JOB_TYPE_UNKNOWN: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_UNKNOWN; + case JOB_TYPE_OTA: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_OTA; + case JOB_TYPE_MAINLINE: + return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_MAINLINE; + default: + throw new IllegalStateException("Unknown job type: " + jobType.getNumber()); + } + } + + private boolean hasUsablePreRebootArtifacts(@NonNull DexoptStatus status) { + // For simplicity, we consider all artifacts of a package usable if we see at least one + // `REASON_PRE_REBOOT_DEXOPT` because it's not easy to know which files are committed. + return status.getDexContainerFileDexoptStatuses().stream().anyMatch(fileStatus + -> fileStatus.getCompilationReason().equals( + ReasonMapping.REASON_PRE_REBOOT_DEXOPT)); + } + + @NonNull + private PreRebootStats.Builder load() { + PreRebootStats.Builder statsBuilder = PreRebootStats.newBuilder(); + try (InputStream in = new FileInputStream(mInjector.getFilename())) { + statsBuilder.mergeFrom(in); + } catch (IOException e) { + // Nothing else we can do but to start from scratch. + AsLog.e("Failed to load pre-reboot stats", e); + } + return statsBuilder; + } + + private void save(@NonNull PreRebootStats.Builder statsBuilder) { + var file = new File(mInjector.getFilename()); + File tempFile = null; + try { + tempFile = File.createTempFile(file.getName(), null /* suffix */, file.getParentFile()); + try (OutputStream out = new FileOutputStream(tempFile.getPath())) { + statsBuilder.build().writeTo(out); + } + Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + AsLog.e("Failed to save pre-reboot stats", e); + } finally { + Utils.deleteIfExistsSafe(tempFile); + } + } + + public void delete() { + Utils.deleteIfExistsSafe(new File(mInjector.getFilename())); + } + + /** + * Injector pattern for testing purpose. + * + * @hide + */ + @VisibleForTesting + public static class Injector { + @NonNull + public String getFilename() { + return FILENAME; + } + + public long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } + + @NonNull + public PackageManagerLocal getPackageManagerLocal() { + return Objects.requireNonNull( + LocalManagerRegistry.getManager(PackageManagerLocal.class)); + } + + @NonNull + public ArtManagerLocal getArtManagerLocal() { + return Objects.requireNonNull(LocalManagerRegistry.getManager(ArtManagerLocal.class)); + } + + @NonNull + public ArtdRefCache.Pin createArtdPin() { + return ArtdRefCache.getInstance().new Pin(); + } + + // Wrap the static void method to make it easier to mock. There is no good way to mock a + // method that is both void and static, due to the poor design of Mockito API. + public void writeStats(int code, int status, int optimizedPackageCount, + int failedPackageCount, int skippedPackageCount, int totalPackageCount, + long jobDurationMillis, long jobLatencyMillis, + int packagesWithArtifactsAfterRebootCount, + int packagesWithArtifactsUsableAfterRebootCount, int jobRunCount, + int packagesWithArtifactsBeforeRebootCount, int jobType) { + ArtStatsLog.write(code, status, optimizedPackageCount, failedPackageCount, + skippedPackageCount, totalPackageCount, jobDurationMillis, jobLatencyMillis, + packagesWithArtifactsAfterRebootCount, + packagesWithArtifactsUsableAfterRebootCount, jobRunCount, + packagesWithArtifactsBeforeRebootCount, jobType); + } + } +} diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java index 1fd7e4bcfc..01e706f0a4 100644 --- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java +++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java @@ -70,6 +70,7 @@ import com.android.server.art.model.DeleteResult; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; import com.android.server.art.model.DexoptStatus; +import com.android.server.art.prereboot.PreRebootStatsReporter; import com.android.server.art.proto.DexMetadataConfig; import com.android.server.art.testing.StaticMockitoRule; import com.android.server.art.testing.TestingUtils; @@ -138,6 +139,7 @@ public class ArtManagerLocalTest { @Mock private DexMetadataHelper.Injector mDexMetadataHelperInjector; @Mock private Context mContext; @Mock private PreRebootDexoptJob mPreRebootDexoptJob; + @Mock private PreRebootStatsReporter.Injector mPreRebootStatsReporterInjector; private PackageState mPkgState1; private AndroidPackage mPkg1; private CheckedSecondaryDexInfo mPkg1SecondaryDexInfo1; @@ -182,6 +184,10 @@ public class ArtManagerLocalTest { lenient().when(mInjector.getDexMetadataHelper()).thenReturn(mDexMetadataHelper); lenient().when(mInjector.getContext()).thenReturn(mContext); lenient().when(mInjector.getPreRebootDexoptJob()).thenReturn(mPreRebootDexoptJob); + lenient() + .when(mInjector.getPreRebootStatsReporter()) + .thenAnswer( + invocation -> new PreRebootStatsReporter(mPreRebootStatsReporterInjector)); lenient().when(mArtFileManagerInjector.getArtd()).thenReturn(mArtd); lenient().when(mArtFileManagerInjector.getUserManager()).thenReturn(mUserManager); @@ -284,6 +290,12 @@ public class ArtManagerLocalTest { .when(mContext.registerReceiver(mBroadcastReceiverCaptor.capture(), any())) .thenReturn(mock(Intent.class)); + File tempFile = File.createTempFile("pre-reboot-stats", ".pb"); + tempFile.deleteOnExit(); + lenient() + .when(mPreRebootStatsReporterInjector.getFilename()) + .thenReturn(tempFile.getAbsolutePath()); + mArtManagerLocal = new ArtManagerLocal(mInjector); } diff --git a/libartservice/service/javatests/com/android/server/art/DexoptHelperTest.java b/libartservice/service/javatests/com/android/server/art/DexoptHelperTest.java index 527203b5a3..8fb46b6225 100644 --- a/libartservice/service/javatests/com/android/server/art/DexoptHelperTest.java +++ b/libartservice/service/javatests/com/android/server/art/DexoptHelperTest.java @@ -76,6 +76,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; @SmallTest @RunWith(AndroidJUnit4.class) @@ -670,15 +671,26 @@ public class DexoptHelperTest { progressCallbackExecutor.runAll(); + List<DexContainerFileDexoptResult> fileResults = + Stream.concat(mPrimaryResults.stream(), mSecondaryResults.stream()) + .collect(Collectors.toList()); + InOrder inOrder = inOrder(progressCallback); inOrder.verify(progressCallback) - .accept(eq(OperationProgress.create(0 /* current */, 3 /* total */))); + .accept(eq(OperationProgress.create( + 0 /* current */, 3 /* total */, null /* packageDexoptResult */))); inOrder.verify(progressCallback) - .accept(eq(OperationProgress.create(1 /* current */, 3 /* total */))); + .accept(eq(OperationProgress.create(1 /* current */, 3 /* total */, + PackageDexoptResult.create( + PKG_NAME_FOO, fileResults, null /* packageLevelStatus */)))); inOrder.verify(progressCallback) - .accept(eq(OperationProgress.create(2 /* current */, 3 /* total */))); + .accept(eq(OperationProgress.create(2 /* current */, 3 /* total */, + PackageDexoptResult.create( + PKG_NAME_BAR, fileResults, null /* packageLevelStatus */)))); inOrder.verify(progressCallback) - .accept(eq(OperationProgress.create(3 /* current */, 3 /* total */))); + .accept(eq(OperationProgress.create(3 /* current */, 3 /* total */, + PackageDexoptResult.create( + PKG_NAME_LIBBAZ, fileResults, null /* packageLevelStatus */)))); } private AndroidPackage createPackage(boolean multiSplit) { diff --git a/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java b/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java index c555879792..eca19160a8 100644 --- a/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java +++ b/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java @@ -22,15 +22,17 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.job.JobInfo; +import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.os.CancellationSignal; import android.os.SystemProperties; @@ -40,18 +42,18 @@ import androidx.test.filters.SmallTest; import com.android.server.art.model.ArtFlags; import com.android.server.art.prereboot.PreRebootDriver; +import com.android.server.art.prereboot.PreRebootStatsReporter; import com.android.server.art.testing.StaticMockitoRule; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.io.File; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -62,12 +64,16 @@ public class PreRebootDexoptJobTest { @Rule public StaticMockitoRule mockitoRule = new StaticMockitoRule( - SystemProperties.class, BackgroundDexoptJobService.class, DeviceConfig.class); + SystemProperties.class, BackgroundDexoptJobService.class, ArtJni.class); @Mock private PreRebootDexoptJob.Injector mInjector; @Mock private JobScheduler mJobScheduler; @Mock private PreRebootDriver mPreRebootDriver; + @Mock private BackgroundDexoptJobService mJobService; + @Mock private PreRebootStatsReporter.Injector mPreRebootStatsReporterInjector; private PreRebootDexoptJob mPreRebootDexoptJob; + private JobInfo mJobInfo; + private JobParameters mJobParameters; @Before public void setUp() throws Exception { @@ -79,12 +85,48 @@ public class PreRebootDexoptJobTest { .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) .thenReturn(true); lenient() - .when(DeviceConfig.getBoolean( + .when(mInjector.getDeviceConfigBoolean( eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean())) .thenReturn(false); + lenient() + .when(SystemProperties.getBoolean( + eq("dalvik.vm.pre-reboot.has-started"), anyBoolean())) + .thenReturn(false); + lenient().when(mInjector.getJobScheduler()).thenReturn(mJobScheduler); lenient().when(mInjector.getPreRebootDriver()).thenReturn(mPreRebootDriver); + lenient() + .when(mInjector.getStatsReporter()) + .thenAnswer( + invocation -> new PreRebootStatsReporter(mPreRebootStatsReporterInjector)); + + File tempFile = File.createTempFile("pre-reboot-stats", ".pb"); + tempFile.deleteOnExit(); + lenient() + .when(mPreRebootStatsReporterInjector.getFilename()) + .thenReturn(tempFile.getAbsolutePath()); + + lenient().when(mJobScheduler.schedule(any())).thenAnswer(invocation -> { + mJobInfo = invocation.<JobInfo>getArgument(0); + mJobParameters = mock(JobParameters.class); + assertThat(mJobInfo.getId()).isEqualTo(JOB_ID); + lenient().when(mJobParameters.getExtras()).thenReturn(mJobInfo.getExtras()); + return JobScheduler.RESULT_SUCCESS; + }); + + lenient() + .doAnswer(invocation -> { + mJobInfo = null; + mJobParameters = null; + return null; + }) + .when(mJobScheduler) + .cancel(JOB_ID); + + lenient().when(mJobScheduler.getPendingJob(JOB_ID)).thenAnswer(invocation -> { + return mJobInfo; + }); mPreRebootDexoptJob = new PreRebootDexoptJob(mInjector); lenient().when(BackgroundDexoptJobService.getJob(JOB_ID)).thenReturn(mPreRebootDexoptJob); @@ -92,16 +134,13 @@ public class PreRebootDexoptJobTest { @Test public void testSchedule() throws Exception { - var captor = ArgumentCaptor.forClass(JobInfo.class); - when(mJobScheduler.schedule(captor.capture())).thenReturn(JobScheduler.RESULT_SUCCESS); + assertThat(mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */)) + .isEqualTo(ArtFlags.SCHEDULE_SUCCESS); - assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS); - - JobInfo jobInfo = captor.getValue(); - assertThat(jobInfo.isPeriodic()).isFalse(); - assertThat(jobInfo.isRequireDeviceIdle()).isTrue(); - assertThat(jobInfo.isRequireCharging()).isTrue(); - assertThat(jobInfo.isRequireBatteryNotLow()).isTrue(); + assertThat(mJobInfo.isPeriodic()).isFalse(); + assertThat(mJobInfo.isRequireDeviceIdle()).isTrue(); + assertThat(mJobInfo.isRequireCharging()).isTrue(); + assertThat(mJobInfo.isRequireBatteryNotLow()).isTrue(); } @Test @@ -109,155 +148,349 @@ public class PreRebootDexoptJobTest { when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean())) .thenReturn(true); - assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); + assertThat(mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */)) + .isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); verify(mJobScheduler, never()).schedule(any()); } @Test + public void testSyncStartDisabled() throws Exception { + when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean())) + .thenReturn(true); + + CompletableFuture<Void> future = + mPreRebootDexoptJob.onUpdateReadyStartNow(null /* otaSlot */); + + assertThat(future).isNull(); + verify(mPreRebootDriver, never()).run(any(), anyBoolean(), any()); + } + + @Test public void testScheduleNotEnabled() { when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) .thenReturn(false); - assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); + assertThat(mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */)) + .isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); verify(mJobScheduler, never()).schedule(any()); } @Test + public void testSyncStartNotEnabled() throws Exception { + when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) + .thenReturn(false); + + CompletableFuture<Void> future = + mPreRebootDexoptJob.onUpdateReadyStartNow(null /* otaSlot */); + + assertThat(future).isNull(); + verify(mPreRebootDriver, never()).run(any(), anyBoolean(), any()); + } + + @Test public void testScheduleEnabledByPhenotypeFlag() { lenient() .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) .thenReturn(false); lenient() - .when(DeviceConfig.getBoolean( + .when(mInjector.getDeviceConfigBoolean( eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean())) .thenReturn(true); - when(mJobScheduler.schedule(any())).thenReturn(JobScheduler.RESULT_SUCCESS); - assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS); + assertThat(mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */)) + .isEqualTo(ArtFlags.SCHEDULE_SUCCESS); verify(mJobScheduler).schedule(any()); } @Test + public void testScheduleForceDisabledByPhenotypeFlag() { + lenient() + .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) + .thenReturn(true); + lenient() + .when(mInjector.getDeviceConfigBoolean( + eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean())) + .thenReturn(true); + when(mInjector.getDeviceConfigBoolean(eq(DeviceConfig.NAMESPACE_RUNTIME), + eq("force_disable_pr_dexopt"), anyBoolean())) + .thenReturn(true); + + assertThat(mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */)) + .isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); + + verify(mJobScheduler, never()).schedule(any()); + } + + @Test public void testUnschedule() { - mPreRebootDexoptJob.unschedule(); + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); verify(mJobScheduler).cancel(JOB_ID); } @Test - public void testStart() { - when(mPreRebootDriver.run(any(), any())).thenReturn(true); + public void testStart() throws Exception { + var jobStarted = new Semaphore(0); + when(mPreRebootDriver.run(any(), eq(true) /* mapSnapshotsForOta */, any())) + .thenAnswer(invocation -> { + jobStarted.release(); + return true; + }); + + when(ArtJni.setProperty("dalvik.vm.pre-reboot.has-started", "true")) + .thenAnswer(invocation -> { + when(SystemProperties.getBoolean( + eq("dalvik.vm.pre-reboot.has-started"), anyBoolean())) + .thenReturn(true); + return null; + }); assertThat(mPreRebootDexoptJob.hasStarted()).isFalse(); - Future<Void> future = mPreRebootDexoptJob.start(); + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + assertThat(jobStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); assertThat(mPreRebootDexoptJob.hasStarted()).isTrue(); - Utils.getFuture(future); + mPreRebootDexoptJob.waitForRunningJob(); } @Test - public void testStartAlreadyRunning() { - Semaphore dexoptDone = new Semaphore(0); - when(mPreRebootDriver.run(any(), any())).thenAnswer(invocation -> { - assertThat(dexoptDone.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); - return true; - }); - - Future<Void> future1 = mPreRebootDexoptJob.start(); - Future<Void> future2 = mPreRebootDexoptJob.start(); - assertThat(future1).isSameInstanceAs(future2); + public void testSyncStart() throws Exception { + when(mPreRebootDriver.run(any(), eq(false) /* mapSnapshotsForOta */, any())) + .thenReturn(true); - dexoptDone.release(); - Utils.getFuture(future1); + CompletableFuture<Void> future = + mPreRebootDexoptJob.onUpdateReadyStartNow(null /* otaSlot */); - verify(mPreRebootDriver, times(1)).run(any(), any()); + Utils.getFuture(future); } @Test - public void testStartAnother() { - when(mPreRebootDriver.run(any(), any())).thenReturn(true); - - Future<Void> future1 = mPreRebootDexoptJob.start(); - Utils.getFuture(future1); - Future<Void> future2 = mPreRebootDexoptJob.start(); - Utils.getFuture(future2); - assertThat(future1).isNotSameInstanceAs(future2); + public void testCancel() { + Semaphore dexoptCancelled = new Semaphore(0); + Semaphore jobExited = new Semaphore(0); + when(mPreRebootDriver.run(any(), anyBoolean(), any())).thenAnswer(invocation -> { + var cancellationSignal = invocation.<CancellationSignal>getArgument(2); + cancellationSignal.setOnCancelListener(() -> dexoptCancelled.release()); + assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); + jobExited.release(); + return true; + }); + + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + mPreRebootDexoptJob.onStopJob(mJobParameters); + + // Check that `onStopJob` is really blocking. If it wasn't, the check below might still pass + // due to a race, but we would have a flaky test. + assertThat(jobExited.tryAcquire()).isTrue(); } @Test - public void testCancel() { + public void testSyncCancel() throws Exception { Semaphore dexoptCancelled = new Semaphore(0); Semaphore jobExited = new Semaphore(0); - when(mPreRebootDriver.run(any(), any())).thenAnswer(invocation -> { + when(mPreRebootDriver.run(any(), anyBoolean(), any())).thenAnswer(invocation -> { + var cancellationSignal = invocation.<CancellationSignal>getArgument(2); + cancellationSignal.setOnCancelListener(() -> dexoptCancelled.release()); assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); - var cancellationSignal = invocation.<CancellationSignal>getArgument(1); - assertThat(cancellationSignal.isCanceled()).isTrue(); jobExited.release(); return true; }); - var unused = mPreRebootDexoptJob.start(); - Future<Void> future = new CompletableFuture().runAsync(() -> { - mPreRebootDexoptJob.cancel(false /* blocking */); - dexoptCancelled.release(); - mPreRebootDexoptJob.cancel(true /* blocking */); - }); - Utils.getFuture(future); - // Check that `cancel(true)` is really blocking. If it wasn't, the check below might still + CompletableFuture<Void> future = + mPreRebootDexoptJob.onUpdateReadyStartNow(null /* otaSlot */); + mPreRebootDexoptJob.cancelGiven(future, false /* expectInterrupt */); + + // Check that `cancelGiven` is really blocking. If it wasn't, the check below might still // pass due to a race, but we would have a flaky test. assertThat(jobExited.tryAcquire()).isTrue(); } @Test public void testUpdateOtaSlotOtaThenMainline() { - mPreRebootDexoptJob.updateOtaSlot("_b"); - mPreRebootDexoptJob.updateOtaSlot(null); + mPreRebootDexoptJob.onUpdateReady("_b" /* otaSlot */); + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); - when(mPreRebootDriver.run(eq("_b"), any())).thenReturn(true); + when(mPreRebootDriver.run(eq("_b"), anyBoolean(), any())).thenReturn(true); - Utils.getFuture(mPreRebootDexoptJob.start()); + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + mPreRebootDexoptJob.waitForRunningJob(); } @Test public void testUpdateOtaSlotMainlineThenOta() { - mPreRebootDexoptJob.updateOtaSlot(null); - mPreRebootDexoptJob.updateOtaSlot("_a"); + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + mPreRebootDexoptJob.onUpdateReady("_a" /* otaSlot */); - when(mPreRebootDriver.run(eq("_a"), any())).thenReturn(true); + when(mPreRebootDriver.run(eq("_a"), anyBoolean(), any())).thenReturn(true); - Utils.getFuture(mPreRebootDexoptJob.start()); + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + mPreRebootDexoptJob.waitForRunningJob(); } @Test public void testUpdateOtaSlotMainlineThenMainline() { - mPreRebootDexoptJob.updateOtaSlot(null); - mPreRebootDexoptJob.updateOtaSlot(null); + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); - when(mPreRebootDriver.run(isNull(), any())).thenReturn(true); + when(mPreRebootDriver.run(isNull(), anyBoolean(), any())).thenReturn(true); - Utils.getFuture(mPreRebootDexoptJob.start()); + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + mPreRebootDexoptJob.waitForRunningJob(); } @Test public void testUpdateOtaSlotOtaThenOta() { - mPreRebootDexoptJob.updateOtaSlot("_b"); - mPreRebootDexoptJob.updateOtaSlot("_b"); + mPreRebootDexoptJob.onUpdateReady("_b" /* otaSlot */); + mPreRebootDexoptJob.onUpdateReady("_b" /* otaSlot */); - when(mPreRebootDriver.run(eq("_b"), any())).thenReturn(true); + when(mPreRebootDriver.run(eq("_b"), anyBoolean(), any())).thenReturn(true); - Utils.getFuture(mPreRebootDexoptJob.start()); + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + mPreRebootDexoptJob.waitForRunningJob(); } @Test(expected = IllegalStateException.class) public void testUpdateOtaSlotOtaThenOtaDifferentSlots() { - mPreRebootDexoptJob.updateOtaSlot("_b"); - mPreRebootDexoptJob.updateOtaSlot("_a"); + mPreRebootDexoptJob.onUpdateReady("_b" /* otaSlot */); + mPreRebootDexoptJob.onUpdateReady("_a" /* otaSlot */); } @Test(expected = IllegalStateException.class) public void testUpdateOtaSlotOtaBogusSlot() { - mPreRebootDexoptJob.updateOtaSlot("_bogus"); + mPreRebootDexoptJob.onUpdateReady("_bogus" /* otaSlot */); + } + + /** + * Verifies that `jobFinished` is not mistakenly called for an old job after a new job is + * started. + */ + @Test + public void testRace1() throws Exception { + var jobBlocker = new Semaphore(0); + + when(mPreRebootDriver.run(any(), anyBoolean(), any())).thenAnswer(invocation -> { + // Simulate that the job takes a while to exit, no matter it's cancelled or not. + assertThat(jobBlocker.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); + return true; + }); + + // An update arrives. A job is scheduled. + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + + // The job scheduler starts the job. + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + + var jobFinishedCalledAfterNewJobStarted = new Semaphore(0); + + var thread = new Thread(() -> { + // Another update arrives. A new job is scheduled, replacing the old job. The old job + // doesn't exit immediately, so this call is blocked. + JobParameters oldParameters = mJobParameters; + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + + // The job scheduler tries to cancel the old job because of the new update. This call + // doesn't matter because the job has already been cancelled by ourselves during the + // `onUpdateReady` call above. + mPreRebootDexoptJob.onStopJob(oldParameters); + + // The job scheduler starts the new job. + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + + doAnswer(invocation -> { + jobFinishedCalledAfterNewJobStarted.release(); + return null; + }) + .when(mJobService) + .jobFinished(any(), anyBoolean()); + }); + thread.start(); + + // Wait a while for `thread` to block on waiting for the old job to exit. + Utils.sleep(200); + + // The old job now exits, unblocking `thread`. + jobBlocker.release(); + thread.join(); + + // Give it 1s for `jobFinished` to be potentially called. Either `jobFinished` is called + // before the new job is started, or it should not be called. + assertThat(jobFinishedCalledAfterNewJobStarted.tryAcquire(1, TimeUnit.SECONDS)).isFalse(); + + // The new job now exits. + jobBlocker.release(); + + // `jobFinished` is called for the new job. + assertThat(jobFinishedCalledAfterNewJobStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)) + .isTrue(); + } + + /** Verifies that `onStartJob` for an old job is ignored after the old job is unscheduled. */ + @Test + public void testRace2() throws Exception { + // An update arrives. A job is scheduled. + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + JobParameters oldParameters = mJobParameters; + + // The job scheduler starts the job. In the meantime, another update arrives. It's not + // possible that `onStartJob` is called for the old job after `onUpdateReady` is called + // because `onUpdateReady` unschedules the old job. However, since both calls acquire a + // lock, the order of execution may be reversed. When this happens, the `onStartJob` request + // should not succeed. + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + assertThat(mPreRebootDexoptJob.onStartJob(mJobService, oldParameters)).isFalse(); + + // The job scheduler starts the new job. This request should succeed. + assertThat(mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters)).isTrue(); + } + + /** + * Verifies that `onStopJob` for an old job is ignored after a new synchronous job is started. + */ + @Test + public void testRace3() throws Exception { + Semaphore dexoptCancelled = new Semaphore(0); + Semaphore jobExited = new Semaphore(0); + when(mPreRebootDriver.run(any(), anyBoolean(), any())).thenAnswer(invocation -> { + var cancellationSignal = invocation.<CancellationSignal>getArgument(2); + cancellationSignal.setOnCancelListener(() -> dexoptCancelled.release()); + assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); + jobExited.release(); + return true; + }); + + // An update arrives. A job is scheduled. + mPreRebootDexoptJob.onUpdateReady(null /* otaSlot */); + + // The job scheduler starts the job. + mPreRebootDexoptJob.onStartJob(mJobService, mJobParameters); + + // Another update arrives, requesting a synchronous job run, replacing the old job. The new + // job, which is synchronous, is started right after the old job is cancelled by + // `onUpdateReadyStartNow`, before the job scheduler calls `onStartJob`. + JobParameters oldParameters = mJobParameters; + CompletableFuture<Void> future = + mPreRebootDexoptJob.onUpdateReadyStartNow(null /* otaSlot */); + + // The old job should be cancelled at this point. + // This cannot be the new job having exited because jobs are serialized. + assertThat(jobExited.tryAcquire()).isTrue(); + + // The `onStopJob` call finally arrives. This call should be a no-op because the job has + // already been cancelled by ourselves during the `onUpdateReadyStartNow` call above. It + // should not cancel the new job. + mPreRebootDexoptJob.onStopJob(oldParameters); + + // The new job should not be cancelled. + assertThat(jobExited.tryAcquire()).isFalse(); + + // Now cancel the new job. + mPreRebootDexoptJob.cancelGiven(future, false /* expectInterrupt */); + + // Now the new job should be cancelled. + assertThat(jobExited.tryAcquire()).isTrue(); } } diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java index 864390c217..49d7c6cb41 100644 --- a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java +++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java @@ -252,6 +252,9 @@ public class PrimaryDexopterParameterizedTest extends PrimaryDexopterTestBase { lenient().when(mArtd.isInDalvikCache(any())).thenReturn(mParams.mIsInDalvikCache); + // By default, no artifacts exist. + lenient().when(mArtd.getArtifactsVisibility(any())).thenReturn(FileVisibility.NOT_FOUND); + if (mParams.mCallbackReturnedCompilerFilter != null) { mConfig.setAdjustCompilerFilterCallback( Runnable::run, (packageName, originalCompilerFilter, reason) -> { @@ -311,6 +314,15 @@ public class PrimaryDexopterParameterizedTest extends PrimaryDexopterTestBase { .when(mDexMetadataHelperInjector.openZipFile(any())) .thenThrow(NoSuchFileException.class); + if (mParams.mIsPreReboot) { + doReturn(FileVisibility.OTHER_READABLE) + .when(mArtd) + .getDexFileVisibility("/somewhere/app/foo/base.apk"); + doReturn(FileVisibility.OTHER_READABLE) + .when(mArtd) + .getDexFileVisibility("/somewhere/app/foo/split_0.apk"); + } + // The first one is normal. doReturn(dexoptIsNeeded()) .when(mArtd) diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java index beb8c1eaf4..0030085497 100644 --- a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java +++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java @@ -127,6 +127,9 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { .when(mDexMetadataHelperInjector.openZipFile(any())) .thenThrow(NoSuchFileException.class); + // By default, no artifacts exist. + lenient().when(mArtd.getArtifactsVisibility(any())).thenReturn(FileVisibility.NOT_FOUND); + // Dexopt is by default needed and successful. lenient() .when(mArtd.getDexoptNeeded(any(), any(), any(), any(), anyInt())) @@ -140,6 +143,13 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { .when(mArtd.createCancellationSignal()) .thenReturn(mock(IArtdCancellationSignal.class)); + lenient() + .when(mArtd.getDexFileVisibility(mDexPath)) + .thenReturn(FileVisibility.OTHER_READABLE); + lenient() + .when(mArtd.getDexFileVisibility(mSplit0DexPath)) + .thenReturn(FileVisibility.OTHER_READABLE); + mPrimaryDexopter = new PrimaryDexopter(mInjector, mPkgState, mPkg, mDexoptParams, mCancellationSignal); @@ -850,6 +860,95 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { .isEqualTo(0); } + @Test + public void testDexoptPreRebootDexNotFound() throws Exception { + when(mInjector.isPreReboot()).thenReturn(true); + doReturn(FileVisibility.NOT_FOUND).when(mArtd).getDexFileVisibility(mDexPath); + doReturn(FileVisibility.NOT_FOUND).when(mArtd).getDexFileVisibility(mSplit0DexPath); + + mPrimaryDexopter = + new PrimaryDexopter(mInjector, mPkgState, mPkg, mDexoptParams, mCancellationSignal); + + List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); + assertThat(results).hasSize(0); + } + + @Test + public void testDexoptPreRebootSomeDexNotFound() throws Exception { + when(mInjector.isPreReboot()).thenReturn(true); + doReturn(FileVisibility.OTHER_READABLE).when(mArtd).getDexFileVisibility(mDexPath); + doReturn(FileVisibility.NOT_FOUND).when(mArtd).getDexFileVisibility(mSplit0DexPath); + + mPrimaryDexopter = + new PrimaryDexopter(mInjector, mPkgState, mPkg, mDexoptParams, mCancellationSignal); + + List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); + assertThat(results).hasSize(2); + assertThat(results.get(0).getDexContainerFile()).isEqualTo(mDexPath); + assertThat(results.get(0).getAbi()).isEqualTo("arm64-v8a"); + assertThat(results.get(1).getDexContainerFile()).isEqualTo(mDexPath); + assertThat(results.get(1).getAbi()).isEqualTo("armeabi-v7a"); + } + + @Test + public void testDexoptPreRebootArtifactsExist() throws Exception { + when(mInjector.isPreReboot()).thenReturn(true); + + when(mArtd.getArtifactsVisibility(deepEq(AidlUtils.buildArtifactsPathAsInputPreReboot( + mDexPath, "arm", false /* isInDalvikCache */)))) + .thenReturn(FileVisibility.OTHER_READABLE); + + mPrimaryDexopter = + new PrimaryDexopter(mInjector, mPkgState, mPkg, mDexoptParams, mCancellationSignal); + + List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); + // Only the one at index 1 is skipped. + assertThat(results).hasSize(4); + assertThat(results.get(0).getDexContainerFile()).isEqualTo(mDexPath); + assertThat(results.get(0).getAbi()).isEqualTo("arm64-v8a"); + assertThat(results.get(0).getStatus()).isEqualTo(DexoptResult.DEXOPT_PERFORMED); + assertThat(results.get(0).getExtendedStatusFlags() + & DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST) + .isEqualTo(0); + assertThat(results.get(1).getDexContainerFile()).isEqualTo(mDexPath); + assertThat(results.get(1).getAbi()).isEqualTo("armeabi-v7a"); + assertThat(results.get(1).getStatus()).isEqualTo(DexoptResult.DEXOPT_SKIPPED); + assertThat(results.get(1).getExtendedStatusFlags() + & DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST) + .isNotEqualTo(0); + assertThat(results.get(2).getDexContainerFile()).isEqualTo(mSplit0DexPath); + assertThat(results.get(2).getAbi()).isEqualTo("arm64-v8a"); + assertThat(results.get(2).getStatus()).isEqualTo(DexoptResult.DEXOPT_PERFORMED); + assertThat(results.get(2).getExtendedStatusFlags() + & DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST) + .isEqualTo(0); + assertThat(results.get(3).getDexContainerFile()).isEqualTo(mSplit0DexPath); + assertThat(results.get(3).getAbi()).isEqualTo("armeabi-v7a"); + assertThat(results.get(3).getStatus()).isEqualTo(DexoptResult.DEXOPT_PERFORMED); + assertThat(results.get(3).getExtendedStatusFlags() + & DexoptResult.EXTENDED_SKIPPED_PRE_REBOOT_ALREADY_EXIST) + .isEqualTo(0); + } + + @Test + public void testDexoptNotAffectedByPreRebootArtifacts() throws Exception { + // Same setup as above, but `isPreReboot` is false. + lenient() + .when(mArtd.getArtifactsVisibility( + deepEq(AidlUtils.buildArtifactsPathAsInputPreReboot( + mDexPath, "arm", false /* isInDalvikCache */)))) + .thenReturn(FileVisibility.OTHER_READABLE); + + mPrimaryDexopter = + new PrimaryDexopter(mInjector, mPkgState, mPkg, mDexoptParams, mCancellationSignal); + + List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); + assertThat(results).hasSize(4); + for (DexContainerFileDexoptResult result : results) { + assertThat(result.getStatus()).isEqualTo(DexoptResult.DEXOPT_PERFORMED); + } + } + private void checkDexoptWithProfile(IArtd artd, String dexPath, String isa, ProfilePath profile, boolean isOtherReadable) throws Exception { artd.dexopt(argThat(artifacts diff --git a/libartservice/service/javatests/com/android/server/art/prereboot/PreRebootStatsReporterTest.java b/libartservice/service/javatests/com/android/server/art/prereboot/PreRebootStatsReporterTest.java new file mode 100644 index 0000000000..4b8571fa6b --- /dev/null +++ b/libartservice/service/javatests/com/android/server/art/prereboot/PreRebootStatsReporterTest.java @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.art.prereboot; + +import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus; +import static com.android.server.art.proto.PreRebootStats.JobRun; +import static com.android.server.art.proto.PreRebootStats.JobType; +import static com.android.server.art.proto.PreRebootStats.Status; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.test.filters.SmallTest; + +import com.android.server.art.ArtManagerLocal; +import com.android.server.art.ArtStatsLog; +import com.android.server.art.model.DexoptStatus; +import com.android.server.art.proto.PreRebootStats; +import com.android.server.pm.PackageManagerLocal; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.List; + +@SmallTest +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class PreRebootStatsReporterTest { + private static final String PKG_NAME_FOO = "com.example.foo"; + private static final String PKG_NAME_BAR = "com.example.bar"; + private static final String PKG_NAME_BAZ = "com.example.baz"; + + @Mock private PreRebootStatsReporter.Injector mInjector; + @Mock private PackageManagerLocal mPackageManagerLocal; + @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot; + @Mock private ArtManagerLocal mArtManagerLocal; + private File mTempFile; + + @Before + public void setUp() throws Exception { + lenient().when(mPackageManagerLocal.withFilteredSnapshot()).thenReturn(mSnapshot); + + mTempFile = File.createTempFile("pre-reboot-stats", ".pb"); + mTempFile.deleteOnExit(); + lenient().when(mInjector.getFilename()).thenReturn(mTempFile.getAbsolutePath()); + + lenient().when(mInjector.getPackageManagerLocal()).thenReturn(mPackageManagerLocal); + lenient().when(mInjector.getArtManagerLocal()).thenReturn(mArtManagerLocal); + } + + @Test + public void testSuccess() throws Exception { + var reporter = new PreRebootStatsReporter(mInjector); + + doReturn(50l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobScheduled(true /* isAsync */, false /* isOtaUpdate */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_SCHEDULED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .build()); + + { + doReturn(200l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobStarted(); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + .setPackagesWithArtifactsBeforeRebootCount(0) + .build()); + + var reporterInChroot = new PreRebootStatsReporter(mInjector); + var progressSession = reporterInChroot.new ProgressSession(); + + progressSession.recordProgress(1 /* skippedPackageCount */, + 2 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 10 /* totalPackageCount */, 4 /* packagesWithArtifactsBeforeRebootCount */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(2) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(4) + .build()); + + doReturn(300l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobEnded(true /* success */, false /* systemRequirementCheckFailed */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_CANCELLED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(200) + .setJobEndedTimestampMillis(300)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(2) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(4) + .build()); + } + + { + doReturn(400l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobStarted(); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(200) + .setJobEndedTimestampMillis(300)) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(400)) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + .setPackagesWithArtifactsBeforeRebootCount(0) + .build()); + + var reporterInChroot = new PreRebootStatsReporter(mInjector); + var progressSession = reporterInChroot.new ProgressSession(); + + progressSession.recordProgress(1 /* skippedPackageCount */, + 6 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 10 /* totalPackageCount */, 8 /* packagesWithArtifactsBeforeRebootCount */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(200) + .setJobEndedTimestampMillis(300)) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(400)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(6) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(8) + .build()); + + doReturn(600l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobEnded(true /* success */, false /* systemRequirementCheckFailed */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_FINISHED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(200) + .setJobEndedTimestampMillis(300)) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(400) + .setJobEndedTimestampMillis(600)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(6) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(8) + .build()); + } + + { + var reporterAfterReboot = new PreRebootStatsReporter(mInjector); + var afterRebootSession = reporterAfterReboot.new AfterRebootSession(); + + // For primary dex. + afterRebootSession.recordPackageWithArtifacts(PKG_NAME_FOO); + afterRebootSession.recordPackageWithArtifacts(PKG_NAME_BAR); + + // For secondary dex. + afterRebootSession.recordPackageWithArtifacts(PKG_NAME_FOO); + afterRebootSession.recordPackageWithArtifacts(PKG_NAME_BAZ); + + // No status has "ab-ota". + doReturn(DexoptStatus.create(List.of(DexContainerFileDexoptStatus.create( + "/somewhere/app/foo/base.apk", true /* isPrimaryDex */, + true /* isPrimaryAbi */, "arm64-v8a", "verify", "vdex", "location")))) + .when(mArtManagerLocal) + .getDexoptStatus(mSnapshot, PKG_NAME_FOO); + + // One status has "ab-ota". + doReturn(DexoptStatus.create(List.of( + DexContainerFileDexoptStatus.create("/somewhere/app/bar/base.apk", + true /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a", + "speed", "ab-ota", "location"), + DexContainerFileDexoptStatus.create("/somewhere/app/bar/base.apk", + true /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a", + "verify", "vdex", "location")))) + .when(mArtManagerLocal) + .getDexoptStatus(mSnapshot, PKG_NAME_BAR); + + // All statuses have "ab-ota". + doReturn(DexoptStatus.create(List.of( + DexContainerFileDexoptStatus.create("/somewhere/app/baz/base.apk", + true /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a", + "verify", "ab-ota", "location"), + DexContainerFileDexoptStatus.create("/somewhere/app/baz/base.apk", + true /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a", + "speed-profile", "ab-ota", "location")))) + .when(mArtManagerLocal) + .getDexoptStatus(mSnapshot, PKG_NAME_BAZ); + + afterRebootSession.report(); + } + + verify(mInjector).writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FINISHED, + 6 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 1 /* skippedPackageCount */, 10 /* totalPackageCount */, + 300 /* jobDurationMillis */, 150 /* jobLatencyMillis */, + 3 /* packagesWithArtifactsAfterRebootCount */, + 2 /* packagesWithArtifactsUsableAfterRebootCount */, 2 /* jobRunCount */, + 8 /* packagesWithArtifactsBeforeRebootCount */, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_MAINLINE); + } + + @Test + public void testSuccessSync() throws Exception { + var reporter = new PreRebootStatsReporter(mInjector); + + reporter.recordJobScheduled(false /* isAsync */, true /* isOtaUpdate */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_SCHEDULED) + .setJobType(JobType.JOB_TYPE_OTA) + .build()); + + { + doReturn(200l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobStarted(); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_OTA) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + .setPackagesWithArtifactsBeforeRebootCount(0) + .build()); + + var reporterInChroot = new PreRebootStatsReporter(mInjector); + var progressSession = reporterInChroot.new ProgressSession(); + + progressSession.recordProgress(1 /* skippedPackageCount */, + 6 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 10 /* totalPackageCount */, 8 /* packagesWithArtifactsBeforeRebootCount */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_OTA) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(6) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(8) + .build()); + + doReturn(300l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobEnded(true /* success */, false /* systemRequirementCheckFailed */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_FINISHED) + .setJobType(JobType.JOB_TYPE_OTA) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(200) + .setJobEndedTimestampMillis(300)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(6) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(8) + .build()); + } + + { + var reporterAfterReboot = new PreRebootStatsReporter(mInjector); + var afterRebootSession = reporterAfterReboot.new AfterRebootSession(); + + afterRebootSession.report(); + } + + verify(mInjector).writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FINISHED, + 6 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 1 /* skippedPackageCount */, 10 /* totalPackageCount */, + 100 /* jobDurationMillis */, -1 /* jobLatencyMillis */, + 0 /* packagesWithArtifactsAfterRebootCount */, + 0 /* packagesWithArtifactsUsableAfterRebootCount */, 1 /* jobRunCount */, + 8 /* packagesWithArtifactsBeforeRebootCount */, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_OTA); + } + + private void checkFailure(boolean systemRequirementCheckFailed) throws Exception { + var reporter = new PreRebootStatsReporter(mInjector); + + doReturn(50l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobScheduled(true /* isAsync */, false /* isOtaUpdate */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_SCHEDULED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .build()); + + { + doReturn(200l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobStarted(); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + .setPackagesWithArtifactsBeforeRebootCount(0) + .build()); + + doReturn(300l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobEnded(false /* success */, systemRequirementCheckFailed); + checkProto(PreRebootStats.newBuilder() + .setStatus(systemRequirementCheckFailed + ? Status.STATUS_ABORTED_SYSTEM_REQUIREMENTS + : Status.STATUS_FAILED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder() + .setJobStartedTimestampMillis(200) + .setJobEndedTimestampMillis(300)) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + .setPackagesWithArtifactsBeforeRebootCount(0) + .build()); + } + + { + var reporterAfterReboot = new PreRebootStatsReporter(mInjector); + var afterRebootSession = reporterAfterReboot.new AfterRebootSession(); + + afterRebootSession.report(); + } + + verify(mInjector).writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED, + systemRequirementCheckFailed + ? ArtStatsLog + .PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORTED_SYSTEM_REQUIREMENTS + : ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FAILED, + 0 /* optimizedPackageCount */, 0 /* failedPackageCount */, + 0 /* skippedPackageCount */, 0 /* totalPackageCount */, 100 /* jobDurationMillis */, + 150 /* jobLatencyMillis */, 0 /* packagesWithArtifactsAfterRebootCount */, + 0 /* packagesWithArtifactsUsableAfterRebootCount */, 1 /* jobRunCount */, + 0 /* packagesWithArtifactsBeforeRebootCount */, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_MAINLINE); + } + + @Test + public void testUnexpectedFailure() throws Exception { + checkFailure(false /* systemRequirementCheckFailed */); + } + + @Test + public void testSystemRequirementCheckFailure() throws Exception { + checkFailure(true /* systemRequirementCheckFailed */); + } + + @Test + public void testStarted() throws Exception { + var reporter = new PreRebootStatsReporter(mInjector); + + doReturn(50l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobScheduled(true /* isAsync */, false /* isOtaUpdate */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_SCHEDULED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .build()); + + { + doReturn(200l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobStarted(); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(0) + .setOptimizedPackageCount(0) + .setFailedPackageCount(0) + .setTotalPackageCount(0) + .setPackagesWithArtifactsBeforeRebootCount(0) + .build()); + + var reporterInChroot = new PreRebootStatsReporter(mInjector); + var progressSession = reporterInChroot.new ProgressSession(); + + progressSession.recordProgress(1 /* skippedPackageCount */, + 2 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 10 /* totalPackageCount */, 4 /* packagesWithArtifactsBeforeRebootCount */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_STARTED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .addJobRuns(JobRun.newBuilder().setJobStartedTimestampMillis(200)) + .setSkippedPackageCount(1) + .setOptimizedPackageCount(2) + .setFailedPackageCount(3) + .setTotalPackageCount(10) + .setPackagesWithArtifactsBeforeRebootCount(4) + .build()); + } + + { + var reporterAfterReboot = new PreRebootStatsReporter(mInjector); + var afterRebootSession = reporterAfterReboot.new AfterRebootSession(); + + afterRebootSession.report(); + } + + verify(mInjector).writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_STARTED, + 2 /* optimizedPackageCount */, 3 /* failedPackageCount */, + 1 /* skippedPackageCount */, 10 /* totalPackageCount */, -1 /* jobDurationMillis */, + 150 /* jobLatencyMillis */, 0 /* packagesWithArtifactsAfterRebootCount */, + 0 /* packagesWithArtifactsUsableAfterRebootCount */, 1 /* jobRunCount */, + 4 /* packagesWithArtifactsBeforeRebootCount */, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_MAINLINE); + } + + @Test + public void testScheduled() throws Exception { + var reporter = new PreRebootStatsReporter(mInjector); + + doReturn(50l).when(mInjector).getCurrentTimeMillis(); + reporter.recordJobScheduled(true /* isAsync */, false /* isOtaUpdate */); + checkProto(PreRebootStats.newBuilder() + .setStatus(Status.STATUS_SCHEDULED) + .setJobType(JobType.JOB_TYPE_MAINLINE) + .setJobScheduledTimestampMillis(50) + .build()); + + { + var reporterAfterReboot = new PreRebootStatsReporter(mInjector); + var afterRebootSession = reporterAfterReboot.new AfterRebootSession(); + + afterRebootSession.report(); + } + + verify(mInjector).writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_SCHEDULED, + 0 /* optimizedPackageCount */, 0 /* failedPackageCount */, + 0 /* skippedPackageCount */, 0 /* totalPackageCount */, -1 /* jobDurationMillis */, + -1 /* jobLatencyMillis */, 0 /* packagesWithArtifactsAfterRebootCount */, + 0 /* packagesWithArtifactsUsableAfterRebootCount */, 0 /* jobRunCount */, + 0 /* packagesWithArtifactsBeforeRebootCount */, + ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_MAINLINE); + } + + private void checkProto(PreRebootStats expected) throws Exception { + PreRebootStats actual; + try (InputStream in = new FileInputStream(mTempFile.getPath())) { + actual = PreRebootStats.parseFrom(in); + } + assertThat(actual).isEqualTo(expected); + } +} diff --git a/libartservice/service/native/service.cc b/libartservice/service/native/service.cc index 5cf51b998a..e97d871dac 100644 --- a/libartservice/service/native/service.cc +++ b/libartservice/service/native/service.cc @@ -23,17 +23,21 @@ #include "android-base/errors.h" #include "android-base/file.h" +#include "android-base/properties.h" #include "android-base/result.h" #include "class_loader_context.h" #include "gc/heap.h" +#include "nativehelper/JNIHelp.h" #include "nativehelper/utils.h" #include "runtime.h" +#include "tools/tools.h" namespace art { namespace service { using ::android::base::Dirname; using ::android::base::Result; +using ::android::base::SetProperty; Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) { if (path_str.empty()) { @@ -134,5 +138,32 @@ Java_com_android_server_art_ArtJni_getGarbageCollectorNative(JNIEnv* env, jobjec return CREATE_UTF_OR_RETURN(env, GetGarbageCollector()).release(); } +extern "C" JNIEXPORT void JNICALL Java_com_android_server_art_ArtJni_setPropertyNative( + JNIEnv* env, jobject, jstring j_key, jstring j_value) { + std::string key(GET_UTF_OR_RETURN_VOID(env, j_key)); + std::string value(GET_UTF_OR_RETURN_VOID(env, j_value)); + if (!SetProperty(key, value)) { + jniThrowExceptionFmt(env, + "java/lang/IllegalStateException", + "Failed to set property '%s' to '%s'", + key.c_str(), + value.c_str()); + } +} + +extern "C" JNIEXPORT void JNICALL Java_com_android_server_art_ArtJni_ensureNoProcessInDirNative( + JNIEnv* env, jobject, jstring j_dir, jint j_timeout_ms) { + if (j_timeout_ms < 0) { + jniThrowExceptionFmt( + env, "java/lang/IllegalArgumentException", "Negative timeout '%d'", j_timeout_ms); + return; + } + std::string dir(GET_UTF_OR_RETURN_VOID(env, j_dir)); + if (Result<void> result = tools::EnsureNoProcessInDir(dir, j_timeout_ms, /*try_kill=*/true); + !result.ok()) { + jniThrowException(env, "java/io/IOException", result.error().message().c_str()); + } +} + } // namespace service } // namespace art diff --git a/libartservice/service/proto/pre_reboot_stats.proto b/libartservice/service/proto/pre_reboot_stats.proto new file mode 100644 index 0000000000..fe42944815 --- /dev/null +++ b/libartservice/service/proto/pre_reboot_stats.proto @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.server.art.proto; +option java_multiple_files = true; + +// Pre-reboot Dexopt metrics to persist on disk for being reported after reboot. +// This proto is persisted on disk and forward and backward compatibility are considerations. +message PreRebootStats { + // Overall status of the job right before the reboot. + // See `android.os.statsd.art.PreRebootDexoptJobEnded`. + enum Status { + STATUS_UNKNOWN = 0; + STATUS_SCHEDULED = 1; + STATUS_STARTED = 2; + STATUS_FAILED = 3; + STATUS_FINISHED = 4; + STATUS_CANCELLED = 5; + STATUS_ABORTED_SYSTEM_REQUIREMENTS = 6; + } + optional Status status = 1; + + // Number of packages successfully optimized. + optional int32 optimized_package_count = 2; + // Number of packages failed to optimize. + optional int32 failed_package_count = 3; + // Number of packages skipped. + optional int32 skipped_package_count = 4; + // Total number of packages scanned. + optional int32 total_package_count = 5; + + // When the job is scheduled, in milliseconds. + optional int64 job_scheduled_timestamp_millis = 6; + + // Represents a job run. + message JobRun { + // When the job is started, in milliseconds. + optional int64 job_started_timestamp_millis = 1; + // When the job is ended (failed, finished, or cancelled), in milliseconds. + optional int64 job_ended_timestamp_millis = 2; + } + + // All job runs. The job may be cancelled and rerun multiple times. + repeated JobRun job_runs = 7; + + // Number of packages that have Pre-reboot Dexopt artifacts before the reboot. Note that this + // isn't necessarily equal to `optimized_package_count` because packages failed to be optimized + // may still have some splits successfully optimized. + optional int32 packages_with_artifacts_before_reboot_count = 8; + + // The type of the job. + enum JobType { + JOB_TYPE_UNKNOWN = 0; + JOB_TYPE_OTA = 1; + JOB_TYPE_MAINLINE = 2; + } + optional JobType job_type = 9; +} diff --git a/libarttools/art_exec_test.cc b/libarttools/art_exec_test.cc index 63f237c342..c1f15dc187 100644 --- a/libarttools/art_exec_test.cc +++ b/libarttools/art_exec_test.cc @@ -17,21 +17,13 @@ #include <sys/capability.h> #include <sys/resource.h> #include <sys/types.h> -#include <sys/wait.h> -#include <unistd.h> -#include <csignal> -#include <filesystem> -#include <functional> #include <string> -#include <utility> #include "android-base/file.h" #include "android-base/logging.h" -#include "android-base/scopeguard.h" #include "android-base/strings.h" #include "base/common_art_test.h" -#include "base/file_utils.h" #include "base/globals.h" #include "base/macros.h" #include "base/os.h" @@ -40,16 +32,16 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "system/thread_defs.h" +#include "testing.h" #ifdef ART_TARGET_ANDROID #include "android-modules-utils/sdk_level.h" #endif namespace art { +namespace tools { namespace { -using ::android::base::make_scope_guard; -using ::android::base::ScopeGuard; using ::android::base::Split; using ::testing::Contains; using ::testing::ElementsAre; @@ -59,45 +51,6 @@ using ::testing::Not; constexpr uid_t kRoot = 0; constexpr uid_t kNobody = 9999; -std::string GetArtBin(const std::string& name) { - return ART_FORMAT("{}/bin/{}", GetArtRoot(), name); -} - -std::string GetBin(const std::string& name) { - return ART_FORMAT("{}/bin/{}", GetAndroidRoot(), name); -} - -// Executes the command, waits for it to finish, and keeps it in a waitable state until the current -// scope exits. -std::pair<pid_t, ScopeGuard<std::function<void()>>> ScopedExecAndWait( - std::vector<std::string>& args) { - std::vector<char*> execv_args; - execv_args.reserve(args.size() + 1); - for (std::string& arg : args) { - execv_args.push_back(arg.data()); - } - execv_args.push_back(nullptr); - - pid_t pid = fork(); - if (pid == 0) { - execv(execv_args[0], execv_args.data()); - UNREACHABLE(); - } else if (pid > 0) { - siginfo_t info; - CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED | WNOWAIT)), 0); - CHECK_EQ(info.si_code, CLD_EXITED); - CHECK_EQ(info.si_status, 0); - std::function<void()> cleanup([=] { - siginfo_t info; - CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED)), 0); - }); - return std::make_pair(pid, make_scope_guard(std::move(cleanup))); - } else { - LOG(FATAL) << "Failed to call fork"; - UNREACHABLE(); - } -} - // Grants the current process the given root capability. void SetCap(cap_flag_t flag, cap_value_t value) { ScopedCap cap(cap_get_proc()); @@ -156,7 +109,7 @@ TEST_F(ArtExecTest, SetTaskProfiles) { GetBin("sh"), "-c", "cat /proc/self/cgroup > " + filename}; - auto [pid, scope_guard] = ScopedExecAndWait(args); + auto [pid, scope_guard] = ScopedExec(args, /*wait=*/true); std::string cgroup; ASSERT_TRUE(android::base::ReadFileToString(filename, &cgroup)); EXPECT_THAT(cgroup, HasSubstr(":cpuset:/foreground\n")); @@ -164,7 +117,7 @@ TEST_F(ArtExecTest, SetTaskProfiles) { TEST_F(ArtExecTest, SetPriority) { std::vector<std::string> args{art_exec_bin_, "--set-priority=background", "--", GetBin("true")}; - auto [pid, scope_guard] = ScopedExecAndWait(args); + auto [pid, scope_guard] = ScopedExec(args, /*wait=*/true); EXPECT_EQ(getpriority(PRIO_PROCESS, pid), ANDROID_PRIORITY_BACKGROUND); } @@ -180,13 +133,13 @@ TEST_F(ArtExecTest, DropCapabilities) { // inherited root capability: CAP_FOWNER). { std::vector<std::string> args{art_exec_bin_, "--", GetBin("true")}; - auto [pid, scope_guard] = ScopedExecAndWait(args); + auto [pid, scope_guard] = ScopedExec(args, /*wait=*/true); ASSERT_TRUE(GetCap(pid, CAP_EFFECTIVE, CAP_FOWNER)); } { std::vector<std::string> args{art_exec_bin_, "--drop-capabilities", "--", GetBin("true")}; - auto [pid, scope_guard] = ScopedExecAndWait(args); + auto [pid, scope_guard] = ScopedExec(args, /*wait=*/true); EXPECT_FALSE(GetCap(pid, CAP_EFFECTIVE, CAP_FOWNER)); } } @@ -218,7 +171,7 @@ TEST_F(ArtExecTest, CloseFds) { file3->Fd(), filename)}; - ScopedExecAndWait(args); + ScopedExec(args, /*wait=*/true); std::string open_fds; ASSERT_TRUE(android::base::ReadFileToString(filename, &open_fds)); @@ -235,7 +188,7 @@ TEST_F(ArtExecTest, Env) { std::vector<std::string> args{ art_exec_bin_, "--env=FOO=BAR", "--", GetBin("sh"), "-c", "env > " + filename}; - ScopedExecAndWait(args); + ScopedExec(args, /*wait=*/true); std::string envs; ASSERT_TRUE(android::base::ReadFileToString(filename, &envs)); @@ -256,7 +209,7 @@ TEST_F(ArtExecTest, ProcessNameSuffix) { "/proc/self/cmdline", filename}; - ScopedExecAndWait(args); + ScopedExec(args, /*wait=*/true); std::string cmdline; ASSERT_TRUE(android::base::ReadFileToString(filename, &cmdline)); @@ -265,4 +218,5 @@ TEST_F(ArtExecTest, ProcessNameSuffix) { } } // namespace +} // namespace tools } // namespace art diff --git a/libarttools/include/tools/system_properties.h b/libarttools/include/tools/system_properties.h index af5aea46e3..5eca765daa 100644 --- a/libarttools/include/tools/system_properties.h +++ b/libarttools/include/tools/system_properties.h @@ -29,6 +29,11 @@ namespace tools { // android::base::GetProperty, this class is mockable. class SystemProperties { public: + SystemProperties() = default; + SystemProperties(const SystemProperties& other) = default; + SystemProperties& operator=(const SystemProperties& other) = default; + SystemProperties(SystemProperties&& other) = default; + SystemProperties& operator=(SystemProperties&& other) = default; virtual ~SystemProperties() = default; // Returns the current value of the system property `key`, or `default_value` if the property diff --git a/libarttools/include/tools/tools.h b/libarttools/include/tools/tools.h index f536e030c4..9399bcd49d 100644 --- a/libarttools/include/tools/tools.h +++ b/libarttools/include/tools/tools.h @@ -17,6 +17,7 @@ #ifndef ART_LIBARTTOOLS_INCLUDE_TOOLS_TOOLS_H_ #define ART_LIBARTTOOLS_INCLUDE_TOOLS_TOOLS_H_ +#include <cstdint> #include <string> #include <string_view> #include <vector> @@ -56,6 +57,11 @@ android::base::Result<std::vector<android::fs_mgr::FstabEntry>> GetProcMountsAnc android::base::Result<std::vector<android::fs_mgr::FstabEntry>> GetProcMountsDescendantsOfPath( std::string_view path); +// See `ArtJni.ensureNoProcessInDir`. +android::base::Result<void> EnsureNoProcessInDir(const std::string& dir, + uint32_t timeout_ms, + bool try_kill); + } // namespace tools } // namespace art diff --git a/libarttools/testing.h b/libarttools/testing.h new file mode 100644 index 0000000000..eaa8523a1b --- /dev/null +++ b/libarttools/testing.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_LIBARTTOOLS_TESTING_H_ +#define ART_LIBARTTOOLS_TESTING_H_ + +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <functional> +#include <string> +#include <utility> +#include <vector> + +#include "android-base/logging.h" +#include "android-base/scopeguard.h" +#include "base/file_utils.h" +#include "base/globals.h" +#include "base/macros.h" + +namespace art { +namespace tools { + +using ::android::base::make_scope_guard; +using ::android::base::ScopeGuard; + +[[maybe_unused]] static std::string GetArtBin(const std::string& name) { + CHECK(kIsTargetAndroid); + return ART_FORMAT("{}/bin/{}", GetArtRoot(), name); +} + +[[maybe_unused]] static std::string GetBin(const std::string& name) { + CHECK(kIsTargetAndroid); + return ART_FORMAT("{}/bin/{}", GetAndroidRoot(), name); +} + +// Executes the command. If the `wait` is true, waits for the process to finish and keeps it in a +// waitable state; otherwise, returns immediately after fork. When the current scope exits, destroys +// the process. +[[maybe_unused]] static std::pair<pid_t, ScopeGuard<std::function<void()>>> ScopedExec( + std::vector<std::string>& args, bool wait) { + std::vector<char*> execv_args; + execv_args.reserve(args.size() + 1); + for (std::string& arg : args) { + execv_args.push_back(arg.data()); + } + execv_args.push_back(nullptr); + + pid_t pid = fork(); + if (pid == 0) { + execv(execv_args[0], execv_args.data()); + UNREACHABLE(); + } else if (pid > 0) { + if (wait) { + siginfo_t info; + CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED | WNOWAIT)), 0); + CHECK_EQ(info.si_code, CLD_EXITED); + CHECK_EQ(info.si_status, 0); + } + std::function<void()> cleanup([=] { + siginfo_t info; + if (!wait) { + CHECK_EQ(kill(pid, SIGKILL), 0); + } + CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED)), 0); + }); + return std::make_pair(pid, make_scope_guard(std::move(cleanup))); + } else { + LOG(FATAL) << "Failed to call fork"; + UNREACHABLE(); + } +} + +} // namespace tools +} // namespace art + +#endif // ART_LIBARTTOOLS_TESTING_H_ diff --git a/libarttools/tools.cc b/libarttools/tools.cc index e355f6d1fa..0dadfde564 100644 --- a/libarttools/tools.cc +++ b/libarttools/tools.cc @@ -18,21 +18,34 @@ #include <errno.h> #include <fnmatch.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <unistd.h> #include <algorithm> +#include <cstdint> +#include <ctime> #include <filesystem> #include <functional> #include <regex> #include <string> #include <string_view> #include <system_error> +#include <unordered_map> +#include <utility> #include <vector> +#include "android-base/errors.h" +#include "android-base/file.h" #include "android-base/function_ref.h" #include "android-base/logging.h" +#include "android-base/process.h" #include "android-base/result.h" #include "android-base/strings.h" +#include "android-base/unique_fd.h" #include "base/macros.h" +#include "base/pidfd.h" #include "fstab/fstab.h" namespace art { @@ -40,15 +53,25 @@ namespace tools { namespace { +using ::android::base::AllPids; using ::android::base::ConsumeSuffix; using ::android::base::function_ref; +using ::android::base::ReadFileToString; +using ::android::base::Readlink; using ::android::base::Result; using ::android::base::StartsWith; +using ::android::base::unique_fd; using ::android::fs_mgr::Fstab; using ::android::fs_mgr::FstabEntry; using ::android::fs_mgr::ReadFstabFromProcMounts; using ::std::placeholders::_1; +uint64_t MilliTime() { + timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return static_cast<uint64_t>(now.tv_sec) * UINT64_C(1000) + now.tv_nsec / UINT64_C(1000000); +} + // Returns true if `path_prefix` matches `pattern` or can be a prefix of a path that matches // `pattern` (i.e., `path_prefix` represents a directory that may contain a file whose path matches // `pattern`). @@ -194,5 +217,105 @@ Result<std::vector<FstabEntry>> GetProcMountsDescendantsOfPath(std::string_view [&](std::string_view mount_point) { return PathStartsWith(mount_point, path); }); } +Result<void> EnsureNoProcessInDir(const std::string& dir, uint32_t timeout_ms, bool try_kill) { + // Pairs of pid and process name, indexed by pidfd. + std::unordered_map<int, std::pair<pid_t, std::string>> running_processes; + std::vector<struct pollfd> pollfds; + std::vector<unique_fd> pidfds; + + for (pid_t pid : AllPids()) { + std::string exe; + if (!Readlink(ART_FORMAT("/proc/{}/exe", pid), &exe)) { + // The caller may not have access to all processes. That's okay. When using this method, we + // must grant the caller access to the processes that we are interested in. + continue; + } + + if (PathStartsWith(exe, dir)) { + unique_fd pidfd = PidfdOpen(pid, /*flags=*/0); + if (pidfd < 0) { + if (errno == ESRCH) { + // The process has gone now. + continue; + } + return ErrnoErrorf("Failed to pidfd_open {}", pid); + } + + std::string name; + if (!ReadFileToString(ART_FORMAT("/proc/{}/comm", pid), &name)) { + PLOG(WARNING) << "Failed to get process name for pid " << pid; + } + size_t pos = name.find_first_of("\n\0"); + if (pos != std::string::npos) { + name.resize(pos); + } + LOG(INFO) << ART_FORMAT( + "Process '{}' (pid: {}) is still running. Waiting for it to exit", name, pid); + + struct pollfd& pollfd = pollfds.emplace_back(); + pollfd.fd = pidfd.get(); + pollfd.events = POLLIN; + + running_processes[pidfd.get()] = std::make_pair(pid, std::move(name)); + pidfds.push_back(std::move(pidfd)); + } + } + + auto wait_for_processes = [&]() -> Result<void> { + uint64_t start_time_ms = MilliTime(); + uint64_t remaining_timeout_ms = timeout_ms; + while (!running_processes.empty() && remaining_timeout_ms > 0) { + int poll_ret = TEMP_FAILURE_RETRY(poll(pollfds.data(), pollfds.size(), remaining_timeout_ms)); + if (poll_ret < 0) { + return ErrnoErrorf("Failed to poll pidfd's"); + } + if (poll_ret == 0) { + // Timeout. + break; + } + uint64_t elapsed_time_ms = MilliTime() - start_time_ms; + for (struct pollfd& pollfd : pollfds) { + if (pollfd.fd < 0) { + continue; + } + if ((pollfd.revents & POLLIN) != 0) { + const auto& [pid, name] = running_processes[pollfd.fd]; + LOG(INFO) << ART_FORMAT( + "Process '{}' (pid: {}) exited in {}ms", name, pid, elapsed_time_ms); + running_processes.erase(pollfd.fd); + pollfd.fd = -1; + } + } + remaining_timeout_ms = timeout_ms - elapsed_time_ms; + } + return {}; + }; + + OR_RETURN(wait_for_processes()); + + bool process_killed = false; + for (const auto& [pidfd, pair] : running_processes) { + const auto& [pid, name] = pair; + LOG(ERROR) << ART_FORMAT( + "Process '{}' (pid: {}) is still running after {}ms", name, pid, timeout_ms); + if (try_kill) { + LOG(INFO) << ART_FORMAT("Killing '{}' (pid: {})", name, pid); + if (kill(pid, SIGKILL) != 0) { + PLOG(ERROR) << ART_FORMAT("Failed to kill '{}' (pid: {})", name, pid); + } + process_killed = true; + } + } + + if (process_killed) { + // Wait another round for processes to exit after being killed. + OR_RETURN(wait_for_processes()); + } + if (!running_processes.empty()) { + return Errorf("Some process(es) are still running after {}ms", timeout_ms); + } + return {}; +} + } // namespace tools } // namespace art diff --git a/libarttools/tools_test.cc b/libarttools/tools_test.cc index 43fa3f42c4..fe2d6d4868 100644 --- a/libarttools/tools_test.cc +++ b/libarttools/tools_test.cc @@ -16,26 +16,32 @@ #include "tools/tools.h" -#include <algorithm> +#include <stdlib.h> +#include <unistd.h> + #include <filesystem> -#include <iterator> #include "android-base/file.h" +#include "android-base/result.h" #include "base/common_art_test.h" +#include "base/globals.h" +#include "base/time_utils.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "testing.h" namespace art { namespace tools { namespace { +using ::android::base::Result; using ::android::base::WriteStringToFile; using ::testing::UnorderedElementsAre; -void CreateFile(const std::string& filename) { +void CreateFile(const std::string& filename, const std::string& content = "") { std::filesystem::path path(filename); std::filesystem::create_directories(path.parent_path()); - ASSERT_TRUE(WriteStringToFile(/*content=*/"", filename)); + ASSERT_TRUE(WriteStringToFile(content, filename)); } class ArtToolsTest : public CommonArtTest { @@ -157,6 +163,99 @@ TEST_F(ArtToolsTest, PathStartsWith) { EXPECT_FALSE(PathStartsWith("/", "/a")); } +class ArtToolsEnsureNoProcessInDirTest : public ArtToolsTest { + protected: + void SetUp() override { + ArtToolsTest::SetUp(); + + related_dir_ = scratch_path_ + "/related"; + unrelated_dir_ = scratch_path_ + "/unrelated"; + + std::string sleep_bin = GetSleepBin(); + if (sleep_bin.empty()) { + GTEST_SKIP() << "'sleep' is not available"; + } + + std::filesystem::create_directories(related_dir_); + std::filesystem::create_directories(unrelated_dir_); + std::filesystem::copy(sleep_bin, related_dir_ + "/sleep"); + std::filesystem::copy(sleep_bin, unrelated_dir_ + "/sleep"); + } + + std::string related_dir_; + std::string unrelated_dir_; + + private: + std::string GetSleepBin() { + if constexpr (kIsTargetAndroid) { + return GetBin("sleep"); + } + if (access("/usr/bin/sleep", X_OK) == 0) { + return "/usr/bin/sleep"; + } + return ""; + } +}; + +TEST_F(ArtToolsEnsureNoProcessInDirTest, WaitsProcesses) { + std::vector<std::string> args_1{related_dir_ + "/sleep", "0.3"}; + auto [pid_1, scope_guard_1] = ScopedExec(args_1, /*wait=*/false); + std::vector<std::string> args_2{unrelated_dir_ + "/sleep", "2"}; + auto [pid_2, scope_guard_2] = ScopedExec(args_2, /*wait=*/false); + NanoSleep(100'000'000); // Wait for child processes to exec. + + ASSERT_RESULT_OK(EnsureNoProcessInDir(related_dir_, /*timeout_ms=*/5000, /*try_kill=*/false)); + + // Check the current status of the process with `WNOHANG`. The related process should have exited, + // so `si_signo` should be `SIGCHLD`. + siginfo_t info; + ASSERT_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid_1, &info, WEXITED | WNOWAIT | WNOHANG)), 0); + EXPECT_EQ(info.si_signo, SIGCHLD); + EXPECT_EQ(info.si_code, CLD_EXITED); + EXPECT_EQ(info.si_status, 0); + + // The method should not wait on unrelated processes. The unrelated process should not have + // exited, so `si_signo` should be 0. + ASSERT_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid_2, &info, WEXITED | WNOWAIT | WNOHANG)), 0); + EXPECT_EQ(info.si_signo, 0); +} + +TEST_F(ArtToolsEnsureNoProcessInDirTest, TimesOut) { + std::vector<std::string> args{related_dir_ + "/sleep", "5"}; + auto [pid, scope_guard] = ScopedExec(args, /*wait=*/false); + NanoSleep(100'000'000); // Wait for child processes to exec. + + Result<void> result = EnsureNoProcessInDir(related_dir_, /*timeout_ms=*/200, /*try_kill=*/false); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.error().message(), "Some process(es) are still running after 200ms"); + + // The process should not have exited. + siginfo_t info; + ASSERT_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED | WNOWAIT | WNOHANG)), 0); + EXPECT_EQ(info.si_signo, 0); +} + +TEST_F(ArtToolsEnsureNoProcessInDirTest, KillsProcesses) { + std::vector<std::string> args_1{related_dir_ + "/sleep", "5"}; + auto [pid_1, scope_guard_1] = ScopedExec(args_1, /*wait=*/false); + std::vector<std::string> args_2{unrelated_dir_ + "/sleep", "5"}; + auto [pid_2, scope_guard_2] = ScopedExec(args_2, /*wait=*/false); + NanoSleep(100'000'000); // Wait for child processes to exec. + + ASSERT_RESULT_OK(EnsureNoProcessInDir(related_dir_, /*timeout_ms=*/200, /*try_kill=*/true)); + + // The related process should have been killed. + siginfo_t info; + ASSERT_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid_1, &info, WEXITED | WNOWAIT | WNOHANG)), 0); + EXPECT_EQ(info.si_signo, SIGCHLD); + EXPECT_EQ(info.si_code, CLD_KILLED); + EXPECT_EQ(info.si_status, SIGKILL); + + // The unrelated process should still be running. + ASSERT_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid_2, &info, WEXITED | WNOWAIT | WNOHANG)), 0); + EXPECT_EQ(info.si_signo, 0); +} + } // namespace } // namespace tools } // namespace art diff --git a/libdexfile/dex/modifiers.h b/libdexfile/dex/modifiers.h index 1307e0eae5..e4fc74a3e7 100644 --- a/libdexfile/dex/modifiers.h +++ b/libdexfile/dex/modifiers.h @@ -57,7 +57,7 @@ static constexpr uint32_t kAccSkipHiddenapiChecks = 0x00100000; // class (run // declaring-class/super-class are to be considered obsolete, meaning they should not be used by. static constexpr uint32_t kAccObsoleteObject = 0x00200000; // class (runtime) // Set during boot image compilation to indicate that the class is -// not initialized at compile tile and not in the list of preloaded classes. +// not initialized at compile time and not in the list of preloaded classes. static constexpr uint32_t kAccInBootImageAndNotInPreloadedClasses = 0x00400000; // class (runtime) // This is set by the class linker during LinkInterfaceMethods. It is used by a method // to represent that it was copied from its declaring class into another class. diff --git a/libnativeloader/public_libraries.cpp b/libnativeloader/public_libraries.cpp index c26b8f6bc4..53fcd2f35b 100644 --- a/libnativeloader/public_libraries.cpp +++ b/libnativeloader/public_libraries.cpp @@ -34,6 +34,7 @@ #include <log/log.h> #if defined(ART_TARGET_ANDROID) +#include <android-modules-utils/sdk_level.h> #include <android/sysprop/VndkProperties.sysprop.h> #endif @@ -426,12 +427,11 @@ const std::map<std::string, std::string>& apex_public_libraries() { bool is_product_treblelized() { #if defined(ART_TARGET_ANDROID) - // Product is not treblelized iff launching version is prior to R and - // ro.product.vndk.version is not defined - static bool product_treblelized = - !(android::base::GetIntProperty("ro.product.first_api_level", 0) < __ANDROID_API_R__ && - !android::sysprop::VndkProperties::product_vndk_version().has_value()); - return product_treblelized; + // Product is treblelized iff the sdk version is newer than U + // or launching version is R or newer or ro.product.vndk.version is defined + return android::modules::sdklevel::IsAtLeastV() || + android::base::GetIntProperty("ro.product.first_api_level", 0) >= __ANDROID_API_R__ || + android::sysprop::VndkProperties::product_vndk_version().has_value(); #else return false; #endif diff --git a/openjdkjvmti/ti_redefine.cc b/openjdkjvmti/ti_redefine.cc index fc1828a56e..3d0c05734c 100644 --- a/openjdkjvmti/ti_redefine.cc +++ b/openjdkjvmti/ti_redefine.cc @@ -2550,6 +2550,9 @@ void Redefiner::ClassRedefinition::UpdateMethods(art::ObjPtr<art::mirror::Class> const art::DexFile& old_dex_file = mclass->GetDexFile(); // Update methods. for (art::ArtMethod& method : mclass->GetDeclaredMethods(image_pointer_size)) { + // Reinitialize the method by calling `CopyFrom`. This ensures for example + // the entrypoint and the hotness are reset. + method.CopyFrom(&method, image_pointer_size); const art::dex::StringId* new_name_id = dex_file_->FindStringId(method.GetName()); art::dex::TypeIndex method_return_idx = dex_file_->GetIndexForTypeId(*dex_file_->FindTypeId(method.GetReturnTypeDescriptorView())); diff --git a/runtime/Android.bp b/runtime/Android.bp index 45d416f0cc..35f37aca54 100644 --- a/runtime/Android.bp +++ b/runtime/Android.bp @@ -949,6 +949,7 @@ art_cc_defaults { ], srcs: [ "common_runtime_test.cc", + "common_transaction_test.cc", "dexopt_test.cc", ], static_libs: [ @@ -1094,6 +1095,7 @@ art_cc_defaults { "intern_table_test.cc", "interpreter/safe_math_test.cc", "interpreter/unstarted_runtime_test.cc", + "interpreter/unstarted_runtime_transaction_test.cc", "jit/jit_memory_region_test.cc", "jit/profile_saver_test.cc", "jit/profiling_info_test.cc", diff --git a/runtime/app_info.cc b/runtime/app_info.cc index 5ec57c7660..50772b1dbf 100644 --- a/runtime/app_info.cc +++ b/runtime/app_info.cc @@ -155,5 +155,17 @@ std::string AppInfo::GetPrimaryApkReferenceProfile() { return ""; } +std::string AppInfo::GetPrimaryApkPath() { + MutexLock mu(Thread::Current(), update_mutex_); + + for (const auto& it : registered_code_locations_) { + const CodeLocationInfo& cli = it.second; + if (cli.code_type == CodeType::kPrimaryApk) { + return it.first; + } + } + return kUnknownValue; +} + } // namespace art diff --git a/runtime/app_info.h b/runtime/app_info.h index 20cec94a78..2bb63774b7 100644 --- a/runtime/app_info.h +++ b/runtime/app_info.h @@ -85,6 +85,13 @@ class AppInfo { // Returns an empty string if there is no primary APK. std::string GetPrimaryApkReferenceProfile(); + // Returns the path of the primary APK. + // If there are multiple primary APKs registed via RegisterAppInfo, the method + // will return the path of the first APK, sorted by the location name. + // + // Returns an empty string if there is no primary APK. + std::string GetPrimaryApkPath(); + // Whether we've received a call to RegisterAppInfo. bool HasRegisteredAppInfo(); diff --git a/runtime/art_method-inl.h b/runtime/art_method-inl.h index b2711d968a..d87040ab49 100644 --- a/runtime/art_method-inl.h +++ b/runtime/art_method-inl.h @@ -777,13 +777,6 @@ inline bool ArtMethod::CounterIsHot() { return hotness_count_ == 0; } -inline bool ArtMethod::CounterHasReached(uint16_t samples, uint16_t threshold) { - DCHECK(!IsAbstract()); - DCHECK_EQ(threshold, Runtime::Current()->GetJITOptions()->GetWarmupThreshold()); - DCHECK_LE(samples, threshold); - return hotness_count_ <= (threshold - samples); -} - inline uint16_t ArtMethod::GetCounter() { DCHECK(!IsAbstract()); return hotness_count_; diff --git a/runtime/art_method.cc b/runtime/art_method.cc index 40022c9d6d..dac2c08a46 100644 --- a/runtime/art_method.cc +++ b/runtime/art_method.cc @@ -660,10 +660,12 @@ const OatQuickMethodHeader* ArtMethod::GetOatQuickMethodHeader(uintptr_t pc) { << ", jit= " << jit; } // We are running the GenericJNI stub. The entrypoint may point - // to different entrypoints or to a JIT-compiled JNI stub. + // to different entrypoints, to a JIT-compiled JNI stub, or to a shared boot + // image stub. DCHECK(class_linker->IsQuickGenericJniStub(existing_entry_point) || class_linker->IsQuickResolutionStub(existing_entry_point) || - (jit != nullptr && jit->GetCodeCache()->ContainsPc(existing_entry_point))) + (jit != nullptr && jit->GetCodeCache()->ContainsPc(existing_entry_point)) || + (class_linker->FindBootJniStub(this) != nullptr)) << " method: " << PrettyMethod() << " entrypoint: " << existing_entry_point << " size: " << OatQuickMethodHeader::FromEntryPoint(existing_entry_point)->GetCodeSize() @@ -794,16 +796,18 @@ void ArtMethod::CopyFrom(ArtMethod* src, PointerSize image_pointer_size) { const void* entry_point = GetEntryPointFromQuickCompiledCodePtrSize(image_pointer_size); if (runtime->UseJitCompilation()) { if (runtime->GetJit()->GetCodeCache()->ContainsPc(entry_point)) { - SetEntryPointFromQuickCompiledCodePtrSize( - src->IsNative() ? GetQuickGenericJniStub() : GetQuickToInterpreterBridge(), - image_pointer_size); + SetNativePointer(EntryPointFromQuickCompiledCodeOffset(image_pointer_size), + src->IsNative() ? GetQuickGenericJniStub() : GetQuickToInterpreterBridge(), + image_pointer_size); } } ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); if (interpreter::IsNterpSupported() && class_linker->IsNterpEntryPoint(entry_point)) { // If the entrypoint is nterp, it's too early to check if the new method // will support it. So for simplicity, use the interpreter bridge. - SetEntryPointFromQuickCompiledCodePtrSize(GetQuickToInterpreterBridge(), image_pointer_size); + SetNativePointer(EntryPointFromQuickCompiledCodeOffset(image_pointer_size), + GetQuickToInterpreterBridge(), + image_pointer_size); } // Clear the data pointer, it will be set if needed by the caller. @@ -920,4 +924,40 @@ ALWAYS_INLINE static inline void DoGetAccessFlagsHelper(ArtMethod* method) method->GetDeclaringClass<kReadBarrierOption>()->IsErroneous()); } +template <typename T> +bool CompareExchange(uintptr_t ptr, uintptr_t old_value, uintptr_t new_value) { + std::atomic<T>* atomic_addr = reinterpret_cast<std::atomic<T>*>(ptr); + T cast_old_value = dchecked_integral_cast<T>(old_value); + return reinterpret_cast<const void*>( + atomic_addr->compare_exchange_strong(cast_old_value, + dchecked_integral_cast<T>(new_value), + std::memory_order_relaxed)); +} + +void ArtMethod::SetEntryPointFromQuickCompiledCodePtrSize( + const void* entry_point_from_quick_compiled_code, PointerSize pointer_size) { + const void* current_entry_point = GetEntryPointFromQuickCompiledCodePtrSize(pointer_size); + if (current_entry_point == entry_point_from_quick_compiled_code) { + return; + } + + // Do an atomic exchange to avoid potentially unregistering JIT code twice. + MemberOffset offset = EntryPointFromQuickCompiledCodeOffset(pointer_size); + uintptr_t old_value = reinterpret_cast<uintptr_t>(current_entry_point); + uintptr_t new_value = reinterpret_cast<uintptr_t>(entry_point_from_quick_compiled_code); + uintptr_t ptr = reinterpret_cast<uintptr_t>(this) + offset.Uint32Value(); + bool success = (pointer_size == PointerSize::k32) + ? CompareExchange<uint32_t>(ptr, old_value, new_value) + : CompareExchange<uint64_t>(ptr, old_value, new_value); + + // If we successfully updated the entrypoint and the old entrypoint is JITted + // code, register the old entrypoint as zombie. + jit::Jit* jit = Runtime::Current()->GetJit(); + if (success && + jit != nullptr && + jit->GetCodeCache()->ContainsPc(current_entry_point)) { + jit->GetCodeCache()->AddZombieCode(this, current_entry_point); + } +} + } // namespace art diff --git a/runtime/art_method.h b/runtime/art_method.h index 96a204055b..0a6cda65fd 100644 --- a/runtime/art_method.h +++ b/runtime/art_method.h @@ -359,11 +359,15 @@ class EXPORT ArtMethod final { } void SetMemorySharedMethod() REQUIRES_SHARED(Locks::mutator_lock_) { - uint32_t access_flags = GetAccessFlags(); - if (!IsIntrinsic(access_flags) && !IsAbstract(access_flags)) { - AddAccessFlags(kAccMemorySharedMethod); - SetHotCounter(); - } + DCHECK(!IsIntrinsic()); + DCHECK(!IsAbstract()); + AddAccessFlags(kAccMemorySharedMethod); + } + + static uint32_t SetMemorySharedMethod(uint32_t access_flags) { + DCHECK(!IsIntrinsic(access_flags)); + DCHECK(!IsAbstract(access_flags)); + return access_flags | kAccMemorySharedMethod; } void ClearMemorySharedMethod() REQUIRES_SHARED(Locks::mutator_lock_) { @@ -597,6 +601,15 @@ class EXPORT ArtMethod final { ClearAccessFlags(kAccNterpInvokeFastPathFlag); } + static uint32_t ClearNterpFastPathFlags(uint32_t access_flags) { + // `kAccNterpEntryPointFastPathFlag` has a different use for native methods. + if (!IsNative(access_flags)) { + access_flags &= ~kAccNterpEntryPointFastPathFlag; + } + access_flags &= ~kAccNterpInvokeFastPathFlag; + return access_flags; + } + // Returns whether the method is a string constructor. The method must not // be a class initializer. (Class initializers are called from a different // context where we do not need to check for string constructors.) @@ -766,11 +779,7 @@ class EXPORT ArtMethod final { } ALWAYS_INLINE void SetEntryPointFromQuickCompiledCodePtrSize( const void* entry_point_from_quick_compiled_code, PointerSize pointer_size) - REQUIRES_SHARED(Locks::mutator_lock_) { - SetNativePointer(EntryPointFromQuickCompiledCodeOffset(pointer_size), - entry_point_from_quick_compiled_code, - pointer_size); - } + REQUIRES_SHARED(Locks::mutator_lock_); static constexpr MemberOffset DataOffset(PointerSize pointer_size) { return MemberOffset(PtrSizedFieldsOffset(pointer_size) + OFFSETOF_MEMBER( @@ -814,6 +823,15 @@ class EXPORT ArtMethod final { return (GetAccessFlags() & kAccSingleImplementation) != 0; } + static uint32_t SetHasSingleImplementation(uint32_t access_flags, bool single_impl) { + DCHECK(!IsIntrinsic(access_flags)) << "conflict with intrinsic bits"; + if (single_impl) { + return access_flags | kAccSingleImplementation; + } else { + return access_flags & ~kAccSingleImplementation; + } + } + // Takes a method and returns a 'canonical' one if the method is default (and therefore // potentially copied from some other class). For example, this ensures that the debugger does not // get confused as to which method we are in. @@ -993,7 +1011,6 @@ class EXPORT ArtMethod final { ALWAYS_INLINE void UpdateCounter(int32_t new_samples); ALWAYS_INLINE void SetHotCounter(); ALWAYS_INLINE bool CounterIsHot(); - ALWAYS_INLINE bool CounterHasReached(uint16_t samples, uint16_t threshold); ALWAYS_INLINE uint16_t GetCounter(); ALWAYS_INLINE bool CounterHasChanged(uint16_t threshold); @@ -1192,6 +1209,8 @@ class EXPORT ArtMethod final { // Used by GetName and GetNameView to share common code. const char* GetRuntimeMethodName() REQUIRES_SHARED(Locks::mutator_lock_); + friend class RuntimeImageHelper; // For SetNativePointer. + DISALLOW_COPY_AND_ASSIGN(ArtMethod); // Need to use CopyFrom to deal with 32 vs 64 bits. }; diff --git a/runtime/class_linker.cc b/runtime/class_linker.cc index 03611ebc20..52caa9f1cf 100644 --- a/runtime/class_linker.cc +++ b/runtime/class_linker.cc @@ -4163,9 +4163,13 @@ void ClassLinker::LoadMethod(const DexFile& dex_file, } } - if (Runtime::Current()->IsZygote() && + if ((access_flags & kAccAbstract) == 0u && + Runtime::Current()->IsZygote() && !Runtime::Current()->GetJITOptions()->GetProfileSaverOptions().GetProfileBootClassPath()) { + DCHECK(!ArtMethod::IsAbstract(access_flags)); + DCHECK(!ArtMethod::IsIntrinsic(access_flags)); dst->SetMemorySharedMethod(); + dst->SetHotCounter(); } } @@ -5857,13 +5861,13 @@ bool ClassLinker::InitializeClass(Thread* self, WrapExceptionInInitializer(klass); mirror::Class::SetStatus(klass, ClassStatus::kErrorResolved, self); success = false; - } else if (Runtime::Current()->IsTransactionAborted()) { + } else if (Runtime::Current()->IsActiveTransaction() && IsTransactionAborted()) { // The exception thrown when the transaction aborted has been caught and cleared // so we need to throw it again now. VLOG(compiler) << "Return from class initializer of " << mirror::Class::PrettyDescriptor(klass.Get()) << " without exception while transaction was aborted: re-throw it now."; - runtime->ThrowTransactionAbortError(self); + ThrowTransactionAbortError(self); mirror::Class::SetStatus(klass, ClassStatus::kErrorResolved, self); success = false; } else { @@ -11168,26 +11172,166 @@ void ClassLinker::SetEnablePublicSdkChecks([[maybe_unused]] bool enabled) { } bool ClassLinker::TransactionWriteConstraint( - [[maybe_unused]] Thread* self, [[maybe_unused]] ObjPtr<mirror::Object> obj) const { + [[maybe_unused]] Thread* self, [[maybe_unused]] ObjPtr<mirror::Object> obj) { // Should not be called on ClassLinker, only on AotClassLinker that overrides this. LOG(FATAL) << "UNREACHABLE"; UNREACHABLE(); } bool ClassLinker::TransactionWriteValueConstraint( - [[maybe_unused]] Thread* self, [[maybe_unused]] ObjPtr<mirror::Object> value) const { + [[maybe_unused]] Thread* self, [[maybe_unused]] ObjPtr<mirror::Object> value) { // Should not be called on ClassLinker, only on AotClassLinker that overrides this. LOG(FATAL) << "UNREACHABLE"; UNREACHABLE(); } bool ClassLinker::TransactionAllocationConstraint( - [[maybe_unused]] Thread* self, [[maybe_unused]] ObjPtr<mirror::Class> klass) const { + [[maybe_unused]] Thread* self, [[maybe_unused]] ObjPtr<mirror::Class> klass) { // Should not be called on ClassLinker, only on AotClassLinker that overrides this. LOG(FATAL) << "UNREACHABLE"; UNREACHABLE(); } +void ClassLinker::RecordWriteFieldBoolean([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] uint8_t value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteFieldByte([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] int8_t value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteFieldChar([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] uint16_t value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteFieldShort([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] int16_t value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteField32([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] uint32_t value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteField64([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] uint64_t value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteFieldReference([[maybe_unused]] mirror::Object* obj, + [[maybe_unused]] MemberOffset field_offset, + [[maybe_unused]] ObjPtr<mirror::Object> value, + [[maybe_unused]] bool is_volatile) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWriteArray([[maybe_unused]] mirror::Array* array, + [[maybe_unused]] size_t index, + [[maybe_unused]] uint64_t value) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordStrongStringInsertion([[maybe_unused]] ObjPtr<mirror::String> s) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWeakStringInsertion([[maybe_unused]] ObjPtr<mirror::String> s) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordStrongStringRemoval([[maybe_unused]] ObjPtr<mirror::String> s) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordWeakStringRemoval([[maybe_unused]] ObjPtr<mirror::String> s) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordResolveString([[maybe_unused]] ObjPtr<mirror::DexCache> dex_cache, + [[maybe_unused]] dex::StringIndex string_idx) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::RecordResolveMethodType([[maybe_unused]] ObjPtr<mirror::DexCache> dex_cache, + [[maybe_unused]] dex::ProtoIndex proto_idx) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::ThrowTransactionAbortError([[maybe_unused]] Thread* self) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::AbortTransactionF( + [[maybe_unused]] Thread* self, [[maybe_unused]] const char* fmt, ...) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::AbortTransactionV([[maybe_unused]] Thread* self, + [[maybe_unused]] const char* fmt, + [[maybe_unused]] va_list args) { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +bool ClassLinker::IsTransactionAborted() const { + // Should not be called on ClassLinker, only on AotClassLinker that overrides this. + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); +} + +void ClassLinker::VisitTransactionRoots([[maybe_unused]] RootVisitor* visitor) { + // Nothing to do for normal `ClassLinker`, only `AotClassLinker` handles transactions. +} + void ClassLinker::RemoveDexFromCaches(const DexFile& dex_file) { ReaderMutexLock mu(Thread::Current(), *Locks::dex_lock_); diff --git a/runtime/class_linker.h b/runtime/class_linker.h index d27664a897..daf9534c3a 100644 --- a/runtime/class_linker.h +++ b/runtime/class_linker.h @@ -879,23 +879,83 @@ class EXPORT ClassLinker { REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(!Locks::dex_lock_, !Roles::uninterruptible_); - // Verifies if the method is accesible according to the SdkChecker (if installed). + // Verifies if the method is accessible according to the SdkChecker (if installed). virtual bool DenyAccessBasedOnPublicSdk(ArtMethod* art_method) const REQUIRES_SHARED(Locks::mutator_lock_); - // Verifies if the field is accesible according to the SdkChecker (if installed). + // Verifies if the field is accessible according to the SdkChecker (if installed). virtual bool DenyAccessBasedOnPublicSdk(ArtField* art_field) const REQUIRES_SHARED(Locks::mutator_lock_); - // Verifies if the descriptor is accesible according to the SdkChecker (if installed). + // Verifies if the descriptor is accessible according to the SdkChecker (if installed). virtual bool DenyAccessBasedOnPublicSdk(std::string_view type_descriptor) const; // Enable or disable public sdk checks. virtual void SetEnablePublicSdkChecks(bool enabled); // Transaction constraint checks for AOT compilation. - virtual bool TransactionWriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) const - REQUIRES_SHARED(Locks::mutator_lock_); - virtual bool TransactionWriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) const - REQUIRES_SHARED(Locks::mutator_lock_); - virtual bool TransactionAllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) const + virtual bool TransactionWriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual bool TransactionWriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual bool TransactionAllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) + REQUIRES_SHARED(Locks::mutator_lock_); + + // Transaction bookkeeping for AOT compilation. + virtual void RecordWriteFieldBoolean(mirror::Object* obj, + MemberOffset field_offset, + uint8_t value, + bool is_volatile); + virtual void RecordWriteFieldByte(mirror::Object* obj, + MemberOffset field_offset, + int8_t value, + bool is_volatile); + virtual void RecordWriteFieldChar(mirror::Object* obj, + MemberOffset field_offset, + uint16_t value, + bool is_volatile); + virtual void RecordWriteFieldShort(mirror::Object* obj, + MemberOffset field_offset, + int16_t value, + bool is_volatile); + virtual void RecordWriteField32(mirror::Object* obj, + MemberOffset field_offset, + uint32_t value, + bool is_volatile); + virtual void RecordWriteField64(mirror::Object* obj, + MemberOffset field_offset, + uint64_t value, + bool is_volatile); + virtual void RecordWriteFieldReference(mirror::Object* obj, + MemberOffset field_offset, + ObjPtr<mirror::Object> value, + bool is_volatile) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual void RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual void RecordStrongStringInsertion(ObjPtr<mirror::String> s) + REQUIRES(Locks::intern_table_lock_); + virtual void RecordWeakStringInsertion(ObjPtr<mirror::String> s) + REQUIRES(Locks::intern_table_lock_); + virtual void RecordStrongStringRemoval(ObjPtr<mirror::String> s) + REQUIRES(Locks::intern_table_lock_); + virtual void RecordWeakStringRemoval(ObjPtr<mirror::String> s) + REQUIRES(Locks::intern_table_lock_); + virtual void RecordResolveString(ObjPtr<mirror::DexCache> dex_cache, dex::StringIndex string_idx) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual void RecordResolveMethodType(ObjPtr<mirror::DexCache> dex_cache, + dex::ProtoIndex proto_idx) + REQUIRES_SHARED(Locks::mutator_lock_); + + // Aborting transactions for AOT compilation. + virtual void ThrowTransactionAbortError(Thread* self) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual void AbortTransactionF(Thread* self, const char* fmt, ...) + __attribute__((__format__(__printf__, 3, 4))) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual void AbortTransactionV(Thread* self, const char* fmt, va_list args) + REQUIRES_SHARED(Locks::mutator_lock_); + virtual bool IsTransactionAborted() const; + + // Vist transaction roots for AOT compilation. + virtual void VisitTransactionRoots(RootVisitor* visitor) REQUIRES_SHARED(Locks::mutator_lock_); void RemoveDexFromCaches(const DexFile& dex_file); diff --git a/runtime/common_runtime_test.cc b/runtime/common_runtime_test.cc index 922d678920..311391dbbf 100644 --- a/runtime/common_runtime_test.cc +++ b/runtime/common_runtime_test.cc @@ -58,6 +58,7 @@ #include "mirror/object_array-alloc-inl.h" #include "native/dalvik_system_DexFile.h" #include "noop_compiler_callbacks.h" +#include "oat/aot_class_linker.h" #include "profile/profile_compilation_info.h" #include "runtime-inl.h" #include "runtime_intrinsics.h" @@ -543,25 +544,6 @@ std::string CommonRuntimeTestImpl::GetSystemImageFile() { return GetImageDirectory() + "/" + isa + "/boot.art"; } -void CommonRuntimeTestImpl::EnterTransactionMode() { - CHECK(!Runtime::Current()->IsActiveTransaction()); - Runtime::Current()->EnterTransactionMode(/*strict=*/ false, /*root=*/ nullptr); -} - -void CommonRuntimeTestImpl::ExitTransactionMode() { - Runtime::Current()->ExitTransactionMode(); - CHECK(!Runtime::Current()->IsActiveTransaction()); -} - -void CommonRuntimeTestImpl::RollbackAndExitTransactionMode() { - Runtime::Current()->RollbackAndExitTransactionMode(); - CHECK(!Runtime::Current()->IsActiveTransaction()); -} - -bool CommonRuntimeTestImpl::IsTransactionAborted() { - return Runtime::Current()->IsTransactionAborted(); -} - void CommonRuntimeTestImpl::VisitDexes(ArrayRef<const std::string> dexes, const std::function<void(MethodReference)>& method_visitor, const std::function<void(TypeReference)>& class_visitor, diff --git a/runtime/common_runtime_test.h b/runtime/common_runtime_test.h index 8f9d7a80ff..87c8d111f0 100644 --- a/runtime/common_runtime_test.h +++ b/runtime/common_runtime_test.h @@ -223,11 +223,6 @@ class CommonRuntimeTestImpl : public CommonArtTestImpl { // Returns the directory where the pre-compiled boot.art can be found. static std::string GetImageLocation(); static std::string GetSystemImageFile(); - - static void EnterTransactionMode() REQUIRES_SHARED(Locks::mutator_lock_); - static void ExitTransactionMode(); - static void RollbackAndExitTransactionMode() REQUIRES_SHARED(Locks::mutator_lock_); - static bool IsTransactionAborted(); }; template <typename TestType> diff --git a/runtime/common_transaction_test.cc b/runtime/common_transaction_test.cc new file mode 100644 index 0000000000..85f176944a --- /dev/null +++ b/runtime/common_transaction_test.cc @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common_transaction_test.h" + +#include "oat/aot_class_linker.h" +#include "runtime.h" + +namespace art HIDDEN { + +void CommonTransactionTestImpl::EnterTransactionMode() { + CHECK(!Runtime::Current()->IsActiveTransaction()); + AotClassLinker* class_linker = down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + class_linker->EnterTransactionMode(/*strict=*/ false, /*root=*/ nullptr); +} + +void CommonTransactionTestImpl::ExitTransactionMode() { + AotClassLinker* class_linker = down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + class_linker->ExitTransactionMode(); + CHECK(!Runtime::Current()->IsActiveTransaction()); +} + +void CommonTransactionTestImpl::RollbackAndExitTransactionMode() { + AotClassLinker* class_linker = down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + class_linker->RollbackAndExitTransactionMode(); + CHECK(!Runtime::Current()->IsActiveTransaction()); +} + +bool CommonTransactionTestImpl::IsTransactionAborted() { + if (!Runtime::Current()->IsActiveTransaction()) { + return false; + } + AotClassLinker* class_linker = down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + return class_linker->IsTransactionAborted(); +} + +} // namespace art diff --git a/runtime/common_transaction_test.h b/runtime/common_transaction_test.h new file mode 100644 index 0000000000..937662009f --- /dev/null +++ b/runtime/common_transaction_test.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_RUNTIME_COMMON_TRANSACTION_TEST_H_ +#define ART_RUNTIME_COMMON_TRANSACTION_TEST_H_ + +#include "common_runtime_test.h" + +namespace art HIDDEN { + +class CommonTransactionTestImpl { + protected: + static void EnterTransactionMode() REQUIRES_SHARED(Locks::mutator_lock_); + static void ExitTransactionMode(); + static void RollbackAndExitTransactionMode() REQUIRES_SHARED(Locks::mutator_lock_); + static bool IsTransactionAborted(); +}; + +template <typename TestType> +class CommonTransactionTestBase : public TestType, public CommonTransactionTestImpl {}; + +using CommonTransactionTest = CommonTransactionTestBase<CommonRuntimeTest>; + +} // namespace art + +#endif // ART_RUNTIME_COMMON_TRANSACTION_TEST_H_ diff --git a/runtime/exec_utils.cc b/runtime/exec_utils.cc index 1266ed18f7..17550d5aae 100644 --- a/runtime/exec_utils.cc +++ b/runtime/exec_utils.cc @@ -17,14 +17,12 @@ #include "exec_utils.h" #include <poll.h> +#include <signal.h> +#include <sys/prctl.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> -#ifdef __BIONIC__ -#include <sys/pidfd.h> -#endif - #include <chrono> #include <climits> #include <condition_variable> @@ -45,6 +43,7 @@ #include "android-base/strings.h" #include "android-base/unique_fd.h" #include "base/macros.h" +#include "base/pidfd.h" #include "base/utils.h" #include "runtime.h" @@ -65,6 +64,10 @@ std::string ToCommandLine(const std::vector<std::string>& args) { // If there is a runtime (Runtime::Current != nullptr) then the subprocess is created with the // same environment that existed when the runtime was started. // Returns the process id of the child process on success, -1 otherwise. +// The child is killed as soon as the caller thread dies, the caller thread needs to stay around and +// wait for the child. Therefore, this function is most suitable to be used by +// `ExecAndReturnResult`, which does the wait. It's not suitable to be used for fire-and-forget +// exec's. pid_t ExecWithoutWait(const std::vector<std::string>& arg_vector, std::string* error_msg) { // Convert the args to char pointers. const char* program = arg_vector[0].c_str(); @@ -83,6 +86,12 @@ pid_t ExecWithoutWait(const std::vector<std::string>& arg_vector, std::string* e // change process groups, so we don't get reaped by ProcessManager setpgid(0, 0); + // Kill the child process when the parent process dies. + if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) { + // This should never happen. + PLOG(FATAL) << "Failed to call prctl"; + } + // (b/30160149): protect subprocesses from modifications to LD_LIBRARY_PATH, etc. // Use the snapshot of the environment from the time the runtime was created. char** envp = (Runtime::Current() == nullptr) ? nullptr : Runtime::Current()->GetEnvSnapshot(); @@ -324,17 +333,7 @@ bool ExecUtils::Exec(const std::vector<std::string>& arg_vector, std::string* er return true; } -unique_fd ExecUtils::PidfdOpen(pid_t pid) const { -#ifdef __BIONIC__ - return unique_fd(pidfd_open(pid, /*flags=*/0)); -#else - // There is no glibc wrapper for pidfd_open. -#ifndef SYS_pidfd_open - constexpr int SYS_pidfd_open = 434; -#endif - return unique_fd(syscall(SYS_pidfd_open, pid, /*flags=*/0)); -#endif -} +unique_fd ExecUtils::PidfdOpen(pid_t pid) const { return art::PidfdOpen(pid, /*flags=*/0); } std::string ExecUtils::GetProcStat(pid_t pid) const { std::string stat_content; diff --git a/runtime/gc/collector/concurrent_copying.cc b/runtime/gc/collector/concurrent_copying.cc index d02701880b..e4b308e846 100644 --- a/runtime/gc/collector/concurrent_copying.cc +++ b/runtime/gc/collector/concurrent_copying.cc @@ -583,7 +583,7 @@ class ConcurrentCopying::FlipCallback : public Closure { if (UNLIKELY(runtime->IsActiveTransaction())) { CHECK(runtime->IsAotCompiler()); TimingLogger::ScopedTiming split3("(Paused)VisitTransactionRoots", cc->GetTimings()); - runtime->VisitTransactionRoots(cc); + runtime->GetClassLinker()->VisitTransactionRoots(cc); } if (kUseBakerReadBarrier && kGrayDirtyImmuneObjects) { cc->GrayAllNewlyDirtyImmuneObjects(); diff --git a/runtime/gc/collector/mark_compact-inl.h b/runtime/gc/collector/mark_compact-inl.h index 18095e8b58..24d47638b1 100644 --- a/runtime/gc/collector/mark_compact-inl.h +++ b/runtime/gc/collector/mark_compact-inl.h @@ -266,9 +266,10 @@ inline bool MarkCompact::VerifyRootSingleUpdate(void* root, mirror::Object* old_ref, const RootInfo& info) { // ASAN promotes stack-frames to heap in order to detect - // stack-use-after-return issues. So skip using this double-root update - // detection on ASAN as well. - if (kIsDebugBuild && !kMemoryToolIsAvailable) { + // stack-use-after-return issues. And HWASAN has pointers tagged, which makes + // it difficult to recognize and prevent stack pointers from being checked. + // So skip using double-root update detection on ASANs. + if (kIsDebugBuild && !kMemoryToolIsAvailable && !kHwAsanEnabled) { void* stack_low_addr = stack_low_addr_; void* stack_high_addr = stack_high_addr_; if (!HasAddress(old_ref)) { @@ -279,15 +280,21 @@ inline bool MarkCompact::VerifyRootSingleUpdate(void* root, stack_low_addr = self->GetStackEnd(); stack_high_addr = reinterpret_cast<char*>(stack_low_addr) + self->GetStackSize(); } - if (root < stack_low_addr || root > stack_high_addr) { + if (std::less<void*>{}(root, stack_low_addr) || std::greater<void*>{}(root, stack_high_addr)) { bool inserted; { MutexLock mu(self, lock_); inserted = updated_roots_->insert(root).second; } - DCHECK(inserted) << "root=" << root << " old_ref=" << old_ref - << " stack_low_addr=" << stack_low_addr - << " stack_high_addr=" << stack_high_addr; + if (!inserted) { + std::ostringstream oss; + heap_->DumpSpaces(oss); + MemMap::DumpMaps(oss, /* terse= */ true); + CHECK(inserted) << "root=" << root << " old_ref=" << old_ref + << " stack_low_addr=" << stack_low_addr + << " stack_high_addr=" << stack_high_addr << " maps\n" + << oss.str(); + } } DCHECK(reinterpret_cast<uint8_t*>(old_ref) >= black_allocations_begin_ || live_words_bitmap_->Test(old_ref)) diff --git a/runtime/gc/collector/mark_compact.cc b/runtime/gc/collector/mark_compact.cc index ac6a7badb4..292ddc09f4 100644 --- a/runtime/gc/collector/mark_compact.cc +++ b/runtime/gc/collector/mark_compact.cc @@ -198,9 +198,10 @@ static gc::CollectorType FetchCmdlineGcType() { std::string argv; gc::CollectorType gc_type = gc::CollectorType::kCollectorTypeNone; if (android::base::ReadFileToString("/proc/self/cmdline", &argv)) { - if (argv.find("-Xgc:CMC") != std::string::npos) { + auto pos = argv.rfind("-Xgc:"); + if (argv.substr(pos + 5, 3) == "CMC") { gc_type = gc::CollectorType::kCollectorTypeCMC; - } else if (argv.find("-Xgc:CC") != std::string::npos) { + } else if (argv.substr(pos + 5, 2) == "CC") { gc_type = gc::CollectorType::kCollectorTypeCC; } } @@ -1066,6 +1067,7 @@ class MarkCompact::ConcurrentCompactionGcTask : public SelfDeletingTask { }; void MarkCompact::PrepareForCompaction() { + TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings()); uint8_t* space_begin = bump_pointer_space_->Begin(); size_t vector_len = (black_allocations_begin_ - space_begin) / kOffsetChunkSize; DCHECK_LE(vector_len, vector_length_); diff --git a/runtime/gc/heap.cc b/runtime/gc/heap.cc index a8c6886e1f..07340bd7d3 100644 --- a/runtime/gc/heap.cc +++ b/runtime/gc/heap.cc @@ -4816,5 +4816,19 @@ std::string Heap::GetForegroundCollectorName() { return oss.str(); } +bool Heap::HasAppImageSpaceFor(const std::string& dex_location) const { + ScopedObjectAccess soa(Thread::Current()); + for (space::ContinuousSpace* space : continuous_spaces_) { + // An image space is either a boot image space or an app image space. + if (space->IsImageSpace() && + !IsBootImageAddress(space->Begin()) && + (space->AsImageSpace()->GetOatFile()->GetOatDexFiles()[0]->GetDexFileLocation() == + dex_location)) { + return true; + } + } + return false; +} + } // namespace gc } // namespace art diff --git a/runtime/gc/heap.h b/runtime/gc/heap.h index 4c99c182c3..c51fcc54e0 100644 --- a/runtime/gc/heap.h +++ b/runtime/gc/heap.h @@ -809,16 +809,7 @@ class Heap { bool HasBootImageSpace() const { return !boot_image_spaces_.empty(); } - bool HasAppImageSpace() const { - ScopedObjectAccess soa(Thread::Current()); - for (const space::ContinuousSpace* space : continuous_spaces_) { - // An image space is either a boot image space or an app image space. - if (space->IsImageSpace() && !IsBootImageAddress(space->Begin())) { - return true; - } - } - return false; - } + bool HasAppImageSpaceFor(const std::string& dex_location) const; ReferenceProcessor* GetReferenceProcessor() { return reference_processor_.get(); diff --git a/runtime/hidden_api_test.cc b/runtime/hidden_api_test.cc index 7dc3d901df..96a5822973 100644 --- a/runtime/hidden_api_test.cc +++ b/runtime/hidden_api_test.cc @@ -42,20 +42,26 @@ static constexpr uint64_t kHideMaxtargetsdkQHiddenApis = 149994052; static constexpr uint64_t kAllowTestApiAccess = 166236554; -static bool Copy(const std::string& src, const std::string& dst, /*out*/ std::string* error_msg) { - std::ifstream src_stream(src, std::ios::binary); - std::ofstream dst_stream(dst, std::ios::binary); - dst_stream << src_stream.rdbuf(); - src_stream.close(); - dst_stream.close(); - if (src_stream.good() && dst_stream.good()) { - return true; - } else { - *error_msg = "Copy " + src + " => " + dst + " (src_good=" - + (src_stream.good() ? "true" : "false") + ", dst_good=" - + (dst_stream.good() ? "true" : "false") + ")"; +static bool Copy(const char* src_filename, File* dst, /*out*/ std::string* error_msg) { + std::unique_ptr<File> src(OS::OpenFileForReading(src_filename)); + if (src == nullptr) { + *error_msg = StringPrintf("Failed to open for reading: %s", src_filename); + return false; + } + int64_t length = src->GetLength(); + if (length < 0) { + *error_msg = "Failed to get file length."; + return false; + } + if (!dst->Copy(src.get(), /*offset=*/ 0, length)) { + *error_msg = "Failed to copy file contents."; + return false; + } + if (dst->Flush() != 0) { + *error_msg = "Failed to flush."; return false; } + return true; } static bool LoadDexFiles(const std::string& path, @@ -91,11 +97,11 @@ static bool LoadDexFiles(const std::string& path, return true; } -static bool Remove(const std::string& path, /*out*/ std::string* error_msg) { - if (TEMP_FAILURE_RETRY(remove(path.c_str())) == 0) { +static bool Remove(const char* path, /*out*/ std::string* error_msg) { + if (TEMP_FAILURE_RETRY(remove(path)) == 0) { return true; } - *error_msg = StringPrintf("Unable to remove(\"%s\"): %s", path.c_str(), strerror(errno)); + *error_msg = StringPrintf("Unable to remove(\"%s\"): %s", path, strerror(errno)); return false; } @@ -191,13 +197,43 @@ class HiddenApiTest : public CommonRuntimeTest { } void TestLocation(const std::string& location, hiddenapi::Domain expected_domain) { + // Create a temp file with a unique name based on `location` to isolate tests + // that may run in parallel. b/238730923 + const std::string_view suffix = ".jar"; + const std::string_view placeholder = "XXXXXX"; // See `mkstemps()`. + ASSERT_TRUE(EndsWith(location, suffix)); + std::unique_ptr<char[]> unique_location(new char[location.length() + placeholder.length() + 1]); + ASSERT_TRUE(unique_location != nullptr); + size_t stem_length = location.length() - suffix.length(); + memcpy(unique_location.get(), location.data(), stem_length); + memcpy(unique_location.get() + stem_length, placeholder.data(), placeholder.length()); + memcpy(unique_location.get() + stem_length + placeholder.length(), + location.data() + stem_length, + suffix.length()); + unique_location[location.length() + placeholder.length()] = 0; + int fd = mkstemps(unique_location.get(), suffix.length()); + ASSERT_TRUE(fd != -1) << strerror(errno); + + // Copy "Main" to the temp file. + std::string error_msg; + { + File file(fd, /*check_usage=*/ true); + bool copied = Copy(GetTestDexFileName("Main").c_str(), &file, &error_msg); + int flush_close_result = file.FlushCloseOrErase(); + if (!copied && flush_close_result == 0) { + // Silently remove the temp file before reporting the `Copy()` failure. + std::string ignored_error_msg; + Remove(unique_location.get(), &ignored_error_msg); + } + ASSERT_TRUE(copied) << error_msg; + ASSERT_EQ(flush_close_result, 0); + } + ScopedObjectAccess soa(Thread::Current()); std::vector<std::unique_ptr<const DexFile>> dex_files; - std::string error_msg; ObjPtr<mirror::ClassLoader> class_loader; - - ASSERT_TRUE(Copy(GetTestDexFileName("Main"), location, &error_msg)) << error_msg; - ASSERT_TRUE(LoadDexFiles(location, soa.Self(), &dex_files, &class_loader, &error_msg)) + ASSERT_TRUE( + LoadDexFiles(unique_location.get(), soa.Self(), &dex_files, &class_loader, &error_msg)) << error_msg; ASSERT_GE(dex_files.size(), 1u); ASSERT_TRUE(CheckAllDexFilesInDomain(class_loader, @@ -206,7 +242,7 @@ class HiddenApiTest : public CommonRuntimeTest { &error_msg)) << error_msg; dex_files.clear(); - ASSERT_TRUE(Remove(location, &error_msg)) << error_msg; + ASSERT_TRUE(Remove(unique_location.get(), &error_msg)) << error_msg; } protected: diff --git a/runtime/intern_table.cc b/runtime/intern_table.cc index 670fd589a2..1986b90a12 100644 --- a/runtime/intern_table.cc +++ b/runtime/intern_table.cc @@ -18,6 +18,7 @@ #include <memory> +#include "class_linker.h" #include "dex/utf.h" #include "gc/collector/garbage_collector.h" #include "gc/space/image_space.h" @@ -149,7 +150,7 @@ void InternTable::AddNewTable() { ObjPtr<mirror::String> InternTable::InsertStrong(ObjPtr<mirror::String> s, uint32_t hash) { Runtime* runtime = Runtime::Current(); if (runtime->IsActiveTransaction()) { - runtime->RecordStrongStringInsertion(s); + runtime->GetClassLinker()->RecordStrongStringInsertion(s); } if (log_new_roots_) { new_strong_intern_roots_.push_back(GcRoot<mirror::String>(s)); @@ -161,7 +162,7 @@ ObjPtr<mirror::String> InternTable::InsertStrong(ObjPtr<mirror::String> s, uint3 ObjPtr<mirror::String> InternTable::InsertWeak(ObjPtr<mirror::String> s, uint32_t hash) { Runtime* runtime = Runtime::Current(); if (runtime->IsActiveTransaction()) { - runtime->RecordWeakStringInsertion(s); + runtime->GetClassLinker()->RecordWeakStringInsertion(s); } weak_interns_.Insert(s, hash); return s; @@ -174,7 +175,7 @@ void InternTable::RemoveStrong(ObjPtr<mirror::String> s, uint32_t hash) { void InternTable::RemoveWeak(ObjPtr<mirror::String> s, uint32_t hash) { Runtime* runtime = Runtime::Current(); if (runtime->IsActiveTransaction()) { - runtime->RecordWeakStringRemoval(s); + runtime->GetClassLinker()->RecordWeakStringRemoval(s); } weak_interns_.Remove(s, hash); } diff --git a/runtime/interpreter/active_transaction_checker.h b/runtime/interpreter/active_transaction_checker.h deleted file mode 100644 index 16fa6b1829..0000000000 --- a/runtime/interpreter/active_transaction_checker.h +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ART_RUNTIME_INTERPRETER_ACTIVE_TRANSACTION_CHECKER_H_ -#define ART_RUNTIME_INTERPRETER_ACTIVE_TRANSACTION_CHECKER_H_ - -#include "base/macros.h" -#include "gc/heap.h" -#include "mirror/object-inl.h" -#include "runtime.h" -#include "transaction.h" - -namespace art HIDDEN { -namespace interpreter { - -class ActiveTransactionChecker { - public: - static inline bool WriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) - REQUIRES_SHARED(Locks::mutator_lock_) { - Runtime* runtime = Runtime::Current(); - if (runtime->GetTransaction()->WriteConstraint(obj)) { - DCHECK(runtime->GetHeap()->ObjectIsInBootImageSpace(obj) || obj->IsClass()); - const char* extra = runtime->GetHeap()->ObjectIsInBootImageSpace(obj) ? "boot image " : ""; - runtime->AbortTransactionF( - self, "Can't set fields of %s%s", extra, obj->PrettyTypeOf().c_str()); - return true; - } - return false; - } - - static inline bool WriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) - REQUIRES_SHARED(Locks::mutator_lock_) { - Runtime* runtime = Runtime::Current(); - if (runtime->GetTransaction()->WriteValueConstraint(value)) { - DCHECK(value != nullptr); - const char* description = value->IsClass() ? "class" : "instance of"; - ObjPtr<mirror::Class> klass = value->IsClass() ? value->AsClass() : value->GetClass(); - runtime->AbortTransactionF( - self, "Can't store reference to %s %s", description, klass->PrettyDescriptor().c_str()); - return true; - } - return false; - } - - static inline bool ReadConstraint(Thread* self, ObjPtr<mirror::Object> obj) - REQUIRES_SHARED(Locks::mutator_lock_) { - DCHECK(obj->IsClass()); - Runtime* runtime = Runtime::Current(); - if (runtime->GetTransaction()->ReadConstraint(obj)) { - runtime->AbortTransactionF( - self, - "Can't read static fields of %s since it does not belong to clinit's class.", - obj->PrettyTypeOf().c_str()); - return true; - } - return false; - } - - static inline bool AllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) - REQUIRES_SHARED(Locks::mutator_lock_) { - if (klass->IsFinalizable()) { - Runtime::Current()->AbortTransactionF(self, - "Allocating finalizable object in transaction: %s", - klass->PrettyDescriptor().c_str()); - return true; - } - return false; - } -}; - -} // namespace interpreter -} // namespace art - -#endif // ART_RUNTIME_INTERPRETER_ACTIVE_TRANSACTION_CHECKER_H_ diff --git a/runtime/interpreter/interpreter.cc b/runtime/interpreter/interpreter.cc index 6097077fe2..fb6f36d5e7 100644 --- a/runtime/interpreter/interpreter.cc +++ b/runtime/interpreter/interpreter.cc @@ -309,7 +309,6 @@ static inline JValue Execute( shadow_frame, ret, instrumentation, - accessor.InsSize(), /* unlock_monitors= */ false); return ret; } @@ -324,7 +323,6 @@ static inline JValue Execute( shadow_frame, ret, instrumentation, - accessor.InsSize(), /* unlock_monitors= */ false); } return ret; diff --git a/runtime/interpreter/interpreter_common.cc b/runtime/interpreter/interpreter_common.cc index 080b32f3d5..f11c62bc71 100644 --- a/runtime/interpreter/interpreter_common.cc +++ b/runtime/interpreter/interpreter_common.cc @@ -20,6 +20,7 @@ #include "base/casts.h" #include "base/pointer_size.h" +#include "class_linker.h" #include "class_root-inl.h" #include "debugger.h" #include "dex/dex_file_types.h" @@ -47,7 +48,7 @@ #include "stack.h" #include "thread-inl.h" #include "var_handles.h" -#include "well_known_classes.h" +#include "well_known_classes-inl.h" namespace art HIDDEN { namespace interpreter { @@ -468,35 +469,48 @@ static bool DoVarHandleInvokeCommon(Thread* self, // Do a quick test for "visibly initialized" without a read barrier and, if that fails, // do a thorough test for "initialized" (including load acquire) with the read barrier. ArtField* field = WellKnownClasses::java_util_concurrent_ThreadLocalRandom_seeder; - if (UNLIKELY(!field->GetDeclaringClass<kWithoutReadBarrier>()->IsVisiblyInitialized()) && - !field->GetDeclaringClass()->IsInitialized()) { - VariableSizedHandleScope callsite_type_hs(self); - mirror::RawMethodType callsite_type(&callsite_type_hs); - if (!class_linker->ResolveMethodType(self, - dex::ProtoIndex(vRegH), - dex_cache, - class_loader, - callsite_type)) { - CHECK(self->IsExceptionPending()); + if (LIKELY(field->GetDeclaringClass<kWithoutReadBarrier>()->IsVisiblyInitialized()) || + field->GetDeclaringClass()->IsInitialized()) { + Handle<mirror::MethodType> callsite_type(hs.NewHandle( + class_linker->ResolveMethodType(self, dex::ProtoIndex(vRegH), dex_cache, class_loader))); + if (LIKELY(callsite_type != nullptr)) { + return VarHandleInvokeAccessor(self, + shadow_frame, + var_handle, + callsite_type, + access_mode, + &operands, + result); + } + // This implies we couldn't resolve one or more types in this VarHandle, + // or we could not allocate the `MethodType` object. + CHECK(self->IsExceptionPending()); + if (self->GetException()->GetClass() != WellKnownClasses::java_lang_OutOfMemoryError.Get()) { return false; } - return VarHandleInvokeAccessor(self, - shadow_frame, - var_handle, - callsite_type, - access_mode, - &operands, - result); - } - - Handle<mirror::MethodType> callsite_type(hs.NewHandle( - class_linker->ResolveMethodType(self, dex::ProtoIndex(vRegH), dex_cache, class_loader))); - // This implies we couldn't resolve one or more types in this VarHandle. - if (UNLIKELY(callsite_type == nullptr)) { + // Clear the OOME and retry without creating an actual `MethodType` object. + // This prevents unexpected OOME for trivial `VarHandle` operations. + // It also prevents odd situations where a `VarHandle` operation succeeds but the same + // operation fails later because the `MethodType` object was evicted from the `DexCache` + // and we suddenly run out of memory to allocate a new one. + // + // We have previously seen OOMEs in the run-test `183-rmw-stress-test` with + // `--optimizng --no-image` (boot class path methods run in interpreter without JIT) + // but it probably happened on the first execution of a trivial `VarHandle` operation + // and not due to the `DexCache` eviction mentioned above. + self->ClearException(); + } + + VariableSizedHandleScope callsite_type_hs(self); + mirror::RawMethodType callsite_type(&callsite_type_hs); + if (!class_linker->ResolveMethodType(self, + dex::ProtoIndex(vRegH), + dex_cache, + class_loader, + callsite_type)) { CHECK(self->IsExceptionPending()); return false; } - return VarHandleInvokeAccessor(self, shadow_frame, var_handle, @@ -1434,7 +1448,7 @@ bool DoCall(ArtMethod* called_method, is_string_init); } -template <bool is_range, bool transaction_active> +template <bool is_range> bool DoFilledNewArray(const Instruction* inst, const ShadowFrame& shadow_frame, Thread* self, @@ -1492,13 +1506,21 @@ bool DoFilledNewArray(const Instruction* inst, } else { inst->GetVarArgs(arg); } - for (int32_t i = 0; i < length; ++i) { - size_t src_reg = is_range ? vregC + i : arg[i]; - if (is_primitive_int_component) { - new_array->AsIntArray()->SetWithoutChecks<transaction_active>( + // We're initializing a newly allocated array, so we do not need to record that under + // a transaction. If the transaction is aborted, the whole array shall be unreachable. + if (LIKELY(is_primitive_int_component)) { + ObjPtr<mirror::IntArray> int_array = new_array->AsIntArray(); + for (int32_t i = 0; i < length; ++i) { + size_t src_reg = is_range ? vregC + i : arg[i]; + int_array->SetWithoutChecks</*kTransactionActive=*/ false, /*kCheckTransaction=*/ false>( i, shadow_frame.GetVReg(src_reg)); - } else { - new_array->AsObjectArray<mirror::Object>()->SetWithoutChecks<transaction_active>( + } + } else { + ObjPtr<mirror::ObjectArray<mirror::Object>> object_array = + new_array->AsObjectArray<mirror::Object>(); + for (int32_t i = 0; i < length; ++i) { + size_t src_reg = is_range ? vregC + i : arg[i]; + object_array->SetWithoutChecks</*kTransactionActive=*/ false, /*kCheckTransaction=*/ false>( i, shadow_frame.GetVRegReference(src_reg)); } } @@ -1507,58 +1529,11 @@ bool DoFilledNewArray(const Instruction* inst, return true; } -// TODO: Use ObjPtr here. -template<typename T> -static void RecordArrayElementsInTransactionImpl(ObjPtr<mirror::PrimitiveArray<T>> array, - int32_t count) - REQUIRES_SHARED(Locks::mutator_lock_) { - Runtime* runtime = Runtime::Current(); - for (int32_t i = 0; i < count; ++i) { - runtime->RecordWriteArray(array.Ptr(), i, array->GetWithoutChecks(i)); - } -} - -void RecordArrayElementsInTransaction(ObjPtr<mirror::Array> array, int32_t count) - REQUIRES_SHARED(Locks::mutator_lock_) { - DCHECK(Runtime::Current()->IsActiveTransaction()); - DCHECK(array != nullptr); - DCHECK_LE(count, array->GetLength()); - Primitive::Type primitive_component_type = array->GetClass()->GetComponentType()->GetPrimitiveType(); - switch (primitive_component_type) { - case Primitive::kPrimBoolean: - RecordArrayElementsInTransactionImpl(array->AsBooleanArray(), count); - break; - case Primitive::kPrimByte: - RecordArrayElementsInTransactionImpl(array->AsByteArray(), count); - break; - case Primitive::kPrimChar: - RecordArrayElementsInTransactionImpl(array->AsCharArray(), count); - break; - case Primitive::kPrimShort: - RecordArrayElementsInTransactionImpl(array->AsShortArray(), count); - break; - case Primitive::kPrimInt: - RecordArrayElementsInTransactionImpl(array->AsIntArray(), count); - break; - case Primitive::kPrimFloat: - RecordArrayElementsInTransactionImpl(array->AsFloatArray(), count); - break; - case Primitive::kPrimLong: - RecordArrayElementsInTransactionImpl(array->AsLongArray(), count); - break; - case Primitive::kPrimDouble: - RecordArrayElementsInTransactionImpl(array->AsDoubleArray(), count); - break; - default: - LOG(FATAL) << "Unsupported primitive type " << primitive_component_type - << " in fill-array-data"; - UNREACHABLE(); - } -} - void UnlockHeldMonitors(Thread* self, ShadowFrame* shadow_frame) REQUIRES_SHARED(Locks::mutator_lock_) { - DCHECK(shadow_frame->GetForcePopFrame() || Runtime::Current()->IsTransactionAborted()); + DCHECK(shadow_frame->GetForcePopFrame() || + (Runtime::Current()->IsActiveTransaction() && + Runtime::Current()->GetClassLinker()->IsTransactionAborted())); // Unlock all monitors. if (shadow_frame->GetMethod()->MustCountLocks()) { DCHECK(!shadow_frame->GetMethod()->SkipAccessChecks()); @@ -1588,6 +1563,26 @@ void UnlockHeldMonitors(Thread* self, ShadowFrame* shadow_frame) } } +void PerformNonStandardReturn(Thread* self, + ShadowFrame& frame, + JValue& result, + const instrumentation::Instrumentation* instrumentation, + bool unlock_monitors) { + if (UNLIKELY(self->IsExceptionPending())) { + LOG(WARNING) << "Suppressing exception for non-standard method exit: " + << self->GetException()->Dump(); + self->ClearException(); + } + if (unlock_monitors) { + UnlockHeldMonitors(self, &frame); + DoMonitorCheckOnExit(self, &frame); + } + result = JValue(); + if (UNLIKELY(NeedsMethodExitEvent(instrumentation))) { + SendMethodExitEvents(self, instrumentation, frame, frame.GetMethod(), result); + } +} + // Explicit DoCall template function declarations. #define EXPLICIT_DO_CALL_TEMPLATE_DECL(_is_range) \ template REQUIRES_SHARED(Locks::mutator_lock_) \ @@ -1613,18 +1608,14 @@ EXPLICIT_DO_INVOKE_POLYMORPHIC_TEMPLATE_DECL(true); #undef EXPLICIT_DO_INVOKE_POLYMORPHIC_TEMPLATE_DECL // Explicit DoFilledNewArray template function declarations. -#define EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL(_is_range_, _transaction_active) \ - template REQUIRES_SHARED(Locks::mutator_lock_) \ - bool DoFilledNewArray<_is_range_, _transaction_active>(const Instruction* inst, \ - const ShadowFrame& shadow_frame, \ - Thread* self, \ - JValue* result) -#define EXPLICIT_DO_FILLED_NEW_ARRAY_ALL_TEMPLATE_DECL(_transaction_active) \ - EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL(false, _transaction_active); \ - EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL(true, _transaction_active) -EXPLICIT_DO_FILLED_NEW_ARRAY_ALL_TEMPLATE_DECL(false); -EXPLICIT_DO_FILLED_NEW_ARRAY_ALL_TEMPLATE_DECL(true); -#undef EXPLICIT_DO_FILLED_NEW_ARRAY_ALL_TEMPLATE_DECL +#define EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL(_is_range_) \ + template REQUIRES_SHARED(Locks::mutator_lock_) \ + bool DoFilledNewArray<_is_range_>(const Instruction* inst, \ + const ShadowFrame& shadow_frame, \ + Thread* self, \ + JValue* result) +EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL(false); +EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL(true); #undef EXPLICIT_DO_FILLED_NEW_ARRAY_TEMPLATE_DECL } // namespace interpreter diff --git a/runtime/interpreter/interpreter_common.h b/runtime/interpreter/interpreter_common.h index 24fb8a83c4..bad54ee448 100644 --- a/runtime/interpreter/interpreter_common.h +++ b/runtime/interpreter/interpreter_common.h @@ -66,38 +66,6 @@ namespace art HIDDEN { namespace interpreter { -// We declare the helper class for transaction checks here but it shall be defined -// only when compiling the transactional interpreter. -class ActiveTransactionChecker; - -// Define the helper class that does not do any transaction checks. -class InactiveTransactionChecker { - public: - ALWAYS_INLINE static bool WriteConstraint([[maybe_unused]] Thread* self, - [[maybe_unused]] ObjPtr<mirror::Object> obj) - REQUIRES_SHARED(Locks::mutator_lock_) { - return false; - } - - ALWAYS_INLINE static bool WriteValueConstraint([[maybe_unused]] Thread* self, - [[maybe_unused]] ObjPtr<mirror::Object> value) - REQUIRES_SHARED(Locks::mutator_lock_) { - return false; - } - - ALWAYS_INLINE static bool ReadConstraint([[maybe_unused]] Thread* self, - [[maybe_unused]] ObjPtr<mirror::Object> value) - REQUIRES_SHARED(Locks::mutator_lock_) { - return false; - } - - ALWAYS_INLINE static bool AllocationConstraint([[maybe_unused]] Thread* self, - [[maybe_unused]] ObjPtr<mirror::Class> klass) - REQUIRES_SHARED(Locks::mutator_lock_) { - return false; - } -}; - void ThrowNullPointerExceptionFromInterpreter() REQUIRES_SHARED(Locks::mutator_lock_); @@ -142,9 +110,6 @@ static inline bool DoMonitorCheckOnExit(Thread* self, ShadowFrame* frame) return true; } -void RecordArrayElementsInTransaction(ObjPtr<mirror::Array> array, int32_t count) - REQUIRES_SHARED(Locks::mutator_lock_); - // Invokes the given method. This is part of the invocation support and is used by DoInvoke, // DoFastInvoke and DoInvokeVirtualQuick functions. // Returns true on success, otherwise throws an exception and returns false. @@ -184,29 +149,11 @@ NeedsMethodExitEvent(const instrumentation::Instrumentation* ins) COLD_ATTR void UnlockHeldMonitors(Thread* self, ShadowFrame* shadow_frame) REQUIRES_SHARED(Locks::mutator_lock_); -static inline ALWAYS_INLINE void PerformNonStandardReturn( - Thread* self, - ShadowFrame& frame, - JValue& result, - const instrumentation::Instrumentation* instrumentation, - uint16_t num_dex_inst, - bool unlock_monitors = true) REQUIRES_SHARED(Locks::mutator_lock_) { - ObjPtr<mirror::Object> thiz(frame.GetThisObject(num_dex_inst)); - StackHandleScope<1u> hs(self); - if (UNLIKELY(self->IsExceptionPending())) { - LOG(WARNING) << "Suppressing exception for non-standard method exit: " - << self->GetException()->Dump(); - self->ClearException(); - } - if (unlock_monitors) { - UnlockHeldMonitors(self, &frame); - DoMonitorCheckOnExit(self, &frame); - } - result = JValue(); - if (UNLIKELY(NeedsMethodExitEvent(instrumentation))) { - SendMethodExitEvents(self, instrumentation, frame, frame.GetMethod(), result); - } -} +void PerformNonStandardReturn(Thread* self, + ShadowFrame& frame, + JValue& result, + const instrumentation::Instrumentation* instrumentation, + bool unlock_monitors = true) REQUIRES_SHARED(Locks::mutator_lock_); // Handles all invoke-XXX/range instructions except for invoke-polymorphic[/range]. // Returns true on success, otherwise throws an exception and returns false. @@ -373,201 +320,6 @@ static inline void GetFieldInfo(Thread* self, } } -// Handles iget-XXX and sget-XXX instructions. -// Returns true on success, otherwise throws an exception and returns false. -template<FindFieldType find_type, - Primitive::Type field_type, - bool transaction_active = false> -ALWAYS_INLINE bool DoFieldGet(Thread* self, - ShadowFrame& shadow_frame, - const Instruction* inst, - uint16_t inst_data) REQUIRES_SHARED(Locks::mutator_lock_) { - const bool is_static = (find_type == StaticObjectRead) || (find_type == StaticPrimitiveRead); - bool should_report = Runtime::Current()->GetInstrumentation()->HasFieldReadListeners(); - ArtField* field = nullptr; - MemberOffset offset(0u); - bool is_volatile; - GetFieldInfo(self, - shadow_frame.GetMethod(), - reinterpret_cast<const uint16_t*>(inst), - is_static, - /*resolve_field_type=*/ false, - &field, - &is_volatile, - &offset); - if (self->IsExceptionPending()) { - return false; - } - - ObjPtr<mirror::Object> obj; - if (is_static) { - obj = field->GetDeclaringClass(); - using TransactionChecker = typename std::conditional_t< - transaction_active, ActiveTransactionChecker, InactiveTransactionChecker>; - if (TransactionChecker::ReadConstraint(self, obj)) { - return false; - } - } else { - obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); - if (should_report || obj == nullptr) { - field = ResolveFieldWithAccessChecks(self, - Runtime::Current()->GetClassLinker(), - inst->VRegC_22c(), - shadow_frame.GetMethod(), - /* is_static= */ false, - /* is_put= */ false, - /* resolve_field_type= */ false); - if (obj == nullptr) { - ThrowNullPointerExceptionForFieldAccess( - field, shadow_frame.GetMethod(), /* is_read= */ true); - return false; - } - // Reload in case suspension happened during field resolution. - obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); - } - } - - uint32_t vregA = is_static ? inst->VRegA_21c(inst_data) : inst->VRegA_22c(inst_data); - JValue result; - if (should_report) { - DCHECK(field != nullptr); - if (UNLIKELY(!DoFieldGetCommon<field_type>(self, shadow_frame, obj, field, &result))) { - // Instrumentation threw an error! - CHECK(self->IsExceptionPending()); - return false; - } - } - -#define FIELD_GET(prim, type, jtype, vreg) \ - case Primitive::kPrim ##prim: \ - shadow_frame.SetVReg ##vreg(vregA, \ - should_report ? result.Get ##jtype() \ - : is_volatile ? obj->GetField ## type ## Volatile(offset) \ - : obj->GetField ##type(offset)); \ - break; - - switch (field_type) { - FIELD_GET(Boolean, Boolean, Z, ) - FIELD_GET(Byte, Byte, B, ) - FIELD_GET(Char, Char, C, ) - FIELD_GET(Short, Short, S, ) - FIELD_GET(Int, 32, I, ) - FIELD_GET(Long, 64, J, Long) -#undef FIELD_GET - case Primitive::kPrimNot: - shadow_frame.SetVRegReference( - vregA, - should_report ? result.GetL() - : is_volatile ? obj->GetFieldObjectVolatile<mirror::Object>(offset) - : obj->GetFieldObject<mirror::Object>(offset)); - break; - default: - LOG(FATAL) << "Unreachable: " << field_type; - UNREACHABLE(); - } - return true; -} - -// Handles iput-XXX and sput-XXX instructions. -// Returns true on success, otherwise throws an exception and returns false. -template<FindFieldType find_type, Primitive::Type field_type, bool transaction_active> -ALWAYS_INLINE bool DoFieldPut(Thread* self, - const ShadowFrame& shadow_frame, - const Instruction* inst, - uint16_t inst_data) - REQUIRES_SHARED(Locks::mutator_lock_) { - bool should_report = Runtime::Current()->GetInstrumentation()->HasFieldWriteListeners(); - bool is_static = (find_type == StaticObjectWrite) || (find_type == StaticPrimitiveWrite); - uint32_t vregA = is_static ? inst->VRegA_21c(inst_data) : inst->VRegA_22c(inst_data); - bool resolve_field_type = (shadow_frame.GetVRegReference(vregA) != nullptr); - ArtField* field = nullptr; - MemberOffset offset(0u); - bool is_volatile; - GetFieldInfo(self, - shadow_frame.GetMethod(), - reinterpret_cast<const uint16_t*>(inst), - is_static, - resolve_field_type, - &field, - &is_volatile, - &offset); - if (self->IsExceptionPending()) { - return false; - } - - ObjPtr<mirror::Object> obj; - if (is_static) { - obj = field->GetDeclaringClass(); - } else { - obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); - if (should_report || obj == nullptr) { - field = ResolveFieldWithAccessChecks(self, - Runtime::Current()->GetClassLinker(), - inst->VRegC_22c(), - shadow_frame.GetMethod(), - /* is_static= */ false, - /* is_put= */ true, - resolve_field_type); - if (UNLIKELY(obj == nullptr)) { - ThrowNullPointerExceptionForFieldAccess( - field, shadow_frame.GetMethod(), /* is_read= */ false); - return false; - } - // Reload in case suspension happened during field resolution. - obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); - } - } - using TransactionChecker = typename std::conditional_t< - transaction_active, ActiveTransactionChecker, InactiveTransactionChecker>; - if (TransactionChecker::WriteConstraint(self, obj)) { - return false; - } - - JValue value = GetFieldValue<field_type>(shadow_frame, vregA); - - if (field_type == Primitive::kPrimNot && - TransactionChecker::WriteValueConstraint(self, value.GetL())) { - return false; - } - if (should_report) { - return DoFieldPutCommon<field_type, transaction_active>(self, - shadow_frame, - obj, - field, - value); - } -#define FIELD_SET(prim, type, jtype) \ - case Primitive::kPrim ## prim: \ - if (is_volatile) { \ - obj->SetField ## type ## Volatile<transaction_active>(offset, value.Get ## jtype()); \ - } else { \ - obj->SetField ## type<transaction_active>(offset, value.Get ## jtype()); \ - } \ - break; - - switch (field_type) { - FIELD_SET(Boolean, Boolean, Z) - FIELD_SET(Byte, Byte, B) - FIELD_SET(Char, Char, C) - FIELD_SET(Short, Short, S) - FIELD_SET(Int, 32, I) - FIELD_SET(Long, 64, J) - FIELD_SET(Not, Object, L) - case Primitive::kPrimVoid: { - LOG(FATAL) << "Unreachable " << field_type; - break; - } - } -#undef FIELD_SET - - if (transaction_active) { - if (UNLIKELY(self->IsExceptionPending())) { - return false; - } - } - return true; -} - // Handles string resolution for const-string and const-string-jumbo instructions. Also ensures the // java.lang.String class is initialized. static inline ObjPtr<mirror::String> ResolveString(Thread* self, @@ -669,7 +421,7 @@ static inline bool DoLongRemainder(ShadowFrame& shadow_frame, // Handles filled-new-array and filled-new-array-range instructions. // Returns true on success, otherwise throws an exception and returns false. -template <bool is_range, bool transaction_active> +template <bool is_range> bool DoFilledNewArray(const Instruction* inst, const ShadowFrame& shadow_frame, Thread* self, diff --git a/runtime/interpreter/interpreter_switch_impl-inl.h b/runtime/interpreter/interpreter_switch_impl-inl.h index 8ff6a897f8..1ebac52c1e 100644 --- a/runtime/interpreter/interpreter_switch_impl-inl.h +++ b/runtime/interpreter/interpreter_switch_impl-inl.h @@ -44,6 +44,218 @@ namespace art HIDDEN { namespace interpreter { +// We declare the helpers classes for transaction checks here but they shall be defined +// only when compiling the transactional and non-transactional interpreter. +class ActiveTransactionChecker; // For transactional interpreter. +class InactiveTransactionChecker; // For non-transactional interpreter. + +// We declare the helpers classes for instrumentation handling here but they shall be defined +// only when compiling the transactional and non-transactional interpreter. +class ActiveInstrumentationHandler; // For non-transactional interpreter. +class InactiveInstrumentationHandler; // For transactional interpreter. + +// Handles iget-XXX and sget-XXX instructions. +// Returns true on success, otherwise throws an exception and returns false. +template<FindFieldType find_type, + Primitive::Type field_type, + bool transaction_active = false> +ALWAYS_INLINE bool DoFieldGet(Thread* self, + ShadowFrame& shadow_frame, + const Instruction* inst, + uint16_t inst_data, + const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + using InstrumentationHandler = typename std::conditional_t< + transaction_active, InactiveInstrumentationHandler, ActiveInstrumentationHandler>; + bool should_report = InstrumentationHandler::HasFieldReadListeners(instrumentation); + const bool is_static = (find_type == StaticObjectRead) || (find_type == StaticPrimitiveRead); + ArtField* field = nullptr; + MemberOffset offset(0u); + bool is_volatile; + GetFieldInfo(self, + shadow_frame.GetMethod(), + reinterpret_cast<const uint16_t*>(inst), + is_static, + /*resolve_field_type=*/ false, + &field, + &is_volatile, + &offset); + if (self->IsExceptionPending()) { + return false; + } + + ObjPtr<mirror::Object> obj; + if (is_static) { + obj = field->GetDeclaringClass(); + using TransactionChecker = typename std::conditional_t< + transaction_active, ActiveTransactionChecker, InactiveTransactionChecker>; + if (TransactionChecker::ReadConstraint(self, obj)) { + return false; + } + } else { + obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); + if (should_report || obj == nullptr) { + field = ResolveFieldWithAccessChecks(self, + Runtime::Current()->GetClassLinker(), + inst->VRegC_22c(), + shadow_frame.GetMethod(), + /* is_static= */ false, + /* is_put= */ false, + /* resolve_field_type= */ false); + if (obj == nullptr) { + ThrowNullPointerExceptionForFieldAccess( + field, shadow_frame.GetMethod(), /* is_read= */ true); + return false; + } + // Reload in case suspension happened during field resolution. + obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); + } + } + + uint32_t vregA = is_static ? inst->VRegA_21c(inst_data) : inst->VRegA_22c(inst_data); + JValue result; + if (should_report) { + DCHECK(field != nullptr); + if (UNLIKELY(!DoFieldGetCommon<field_type>(self, shadow_frame, obj, field, &result))) { + // Instrumentation threw an error! + CHECK(self->IsExceptionPending()); + return false; + } + } + +#define FIELD_GET(prim, type, jtype, vreg) \ + case Primitive::kPrim ##prim: \ + shadow_frame.SetVReg ##vreg(vregA, \ + should_report ? result.Get ##jtype() \ + : is_volatile ? obj->GetField ## type ## Volatile(offset) \ + : obj->GetField ##type(offset)); \ + break; + + switch (field_type) { + FIELD_GET(Boolean, Boolean, Z, ) + FIELD_GET(Byte, Byte, B, ) + FIELD_GET(Char, Char, C, ) + FIELD_GET(Short, Short, S, ) + FIELD_GET(Int, 32, I, ) + FIELD_GET(Long, 64, J, Long) +#undef FIELD_GET + case Primitive::kPrimNot: + shadow_frame.SetVRegReference( + vregA, + should_report ? result.GetL() + : is_volatile ? obj->GetFieldObjectVolatile<mirror::Object>(offset) + : obj->GetFieldObject<mirror::Object>(offset)); + break; + default: + LOG(FATAL) << "Unreachable: " << field_type; + UNREACHABLE(); + } + return true; +} + +// Handles iput-XXX and sput-XXX instructions. +// Returns true on success, otherwise throws an exception and returns false. +template<FindFieldType find_type, Primitive::Type field_type, bool transaction_active> +ALWAYS_INLINE bool DoFieldPut(Thread* self, + const ShadowFrame& shadow_frame, + const Instruction* inst, + uint16_t inst_data, + const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + using InstrumentationHandler = typename std::conditional_t< + transaction_active, InactiveInstrumentationHandler, ActiveInstrumentationHandler>; + bool should_report = InstrumentationHandler::HasFieldWriteListeners(instrumentation); + bool is_static = (find_type == StaticObjectWrite) || (find_type == StaticPrimitiveWrite); + uint32_t vregA = is_static ? inst->VRegA_21c(inst_data) : inst->VRegA_22c(inst_data); + bool resolve_field_type = (shadow_frame.GetVRegReference(vregA) != nullptr); + ArtField* field = nullptr; + MemberOffset offset(0u); + bool is_volatile; + GetFieldInfo(self, + shadow_frame.GetMethod(), + reinterpret_cast<const uint16_t*>(inst), + is_static, + resolve_field_type, + &field, + &is_volatile, + &offset); + if (self->IsExceptionPending()) { + return false; + } + + ObjPtr<mirror::Object> obj; + if (is_static) { + obj = field->GetDeclaringClass(); + } else { + obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); + if (should_report || obj == nullptr) { + field = ResolveFieldWithAccessChecks(self, + Runtime::Current()->GetClassLinker(), + inst->VRegC_22c(), + shadow_frame.GetMethod(), + /* is_static= */ false, + /* is_put= */ true, + resolve_field_type); + if (UNLIKELY(obj == nullptr)) { + ThrowNullPointerExceptionForFieldAccess( + field, shadow_frame.GetMethod(), /* is_read= */ false); + return false; + } + // Reload in case suspension happened during field resolution. + obj = shadow_frame.GetVRegReference(inst->VRegB_22c(inst_data)); + } + } + using TransactionChecker = typename std::conditional_t< + transaction_active, ActiveTransactionChecker, InactiveTransactionChecker>; + if (TransactionChecker::WriteConstraint(self, obj)) { + return false; + } + + JValue value = GetFieldValue<field_type>(shadow_frame, vregA); + + if (field_type == Primitive::kPrimNot && + TransactionChecker::WriteValueConstraint(self, value.GetL())) { + return false; + } + if (should_report) { + return DoFieldPutCommon<field_type, transaction_active>(self, + shadow_frame, + obj, + field, + value); + } +#define FIELD_SET(prim, type, jtype) \ + case Primitive::kPrim ## prim: \ + if (is_volatile) { \ + obj->SetField ## type ## Volatile<transaction_active>(offset, value.Get ## jtype()); \ + } else { \ + obj->SetField ## type<transaction_active>(offset, value.Get ## jtype()); \ + } \ + break; + + switch (field_type) { + FIELD_SET(Boolean, Boolean, Z) + FIELD_SET(Byte, Byte, B) + FIELD_SET(Char, Char, C) + FIELD_SET(Short, Short, S) + FIELD_SET(Int, 32, I) + FIELD_SET(Long, 64, J) + FIELD_SET(Not, Object, L) + case Primitive::kPrimVoid: { + LOG(FATAL) << "Unreachable " << field_type; + break; + } + } +#undef FIELD_SET + + if (transaction_active) { + if (UNLIKELY(self->IsExceptionPending())) { + return false; + } + } + return true; +} + // Short-lived helper class which executes single DEX bytecode. It is inlined by compiler. // Any relevant execution information is stored in the fields - it should be kept to minimum. // All instance functions must be inlined so that the fields can be stored in registers. @@ -54,13 +266,15 @@ namespace interpreter { template<bool transaction_active, Instruction::Format kFormat> class InstructionHandler { public: + using InstrumentationHandler = typename std::conditional_t< + transaction_active, InactiveInstrumentationHandler, ActiveInstrumentationHandler>; using TransactionChecker = typename std::conditional_t< transaction_active, ActiveTransactionChecker, InactiveTransactionChecker>; #define HANDLER_ATTRIBUTES ALWAYS_INLINE FLATTEN WARN_UNUSED REQUIRES_SHARED(Locks::mutator_lock_) HANDLER_ATTRIBUTES bool CheckTransactionAbort() { - if (transaction_active && Runtime::Current()->IsTransactionAborted()) { + if (TransactionChecker::IsTransactionAborted()) { // Transaction abort cannot be caught by catch handlers. // Preserve the abort exception while doing non-standard return. StackHandleScope<1u> hs(Self()); @@ -68,8 +282,7 @@ class InstructionHandler { DCHECK(abort_exception != nullptr); DCHECK(abort_exception->GetClass()->DescriptorEquals(kTransactionAbortErrorDescriptor)); Self()->ClearException(); - PerformNonStandardReturn( - Self(), shadow_frame_, ctx_->result, Instrumentation(), Accessor().InsSize()); + PerformNonStandardReturn(Self(), shadow_frame_, ctx_->result, Instrumentation()); Self()->SetException(abort_exception.Get()); ExitInterpreterLoop(); return false; @@ -78,10 +291,9 @@ class InstructionHandler { } HANDLER_ATTRIBUTES bool CheckForceReturn() { - if (shadow_frame_.GetForcePopFrame()) { + if (InstrumentationHandler::GetForcePopFrame(shadow_frame_)) { DCHECK(Runtime::Current()->AreNonStandardExitsEnabled()); - PerformNonStandardReturn( - Self(), shadow_frame_, ctx_->result, Instrumentation(), Accessor().InsSize()); + PerformNonStandardReturn(Self(), shadow_frame_, ctx_->result, Instrumentation()); ExitInterpreterLoop(); return false; } @@ -148,16 +360,16 @@ class InstructionHandler { if (!CheckForceReturn()) { return false; } - if (UNLIKELY(shadow_frame_.GetNotifyDexPcMoveEvents())) { + if (UNLIKELY(InstrumentationHandler::NeedsDexPcEvents(shadow_frame_))) { uint8_t opcode = inst_->Opcode(inst_data_); bool is_move_result_object = (opcode == Instruction::MOVE_RESULT_OBJECT); JValue* save_ref = is_move_result_object ? &ctx_->result_register : nullptr; - if (UNLIKELY(!DoDexPcMoveEvent(Self(), - Accessor(), - shadow_frame_, - DexPC(), - Instrumentation(), - save_ref))) { + if (UNLIKELY(!InstrumentationHandler::DoDexPcMoveEvent(Self(), + Accessor(), + shadow_frame_, + DexPC(), + Instrumentation(), + save_ref))) { DCHECK(Self()->IsExceptionPending()); // Do not raise exception event if it is caused by other instrumentation event. shadow_frame_.SetSkipNextExceptionEvent(true); @@ -170,53 +382,17 @@ class InstructionHandler { return true; } - // Unlike most other events the DexPcMovedEvent can be sent when there is a pending exception (if - // the next instruction is MOVE_EXCEPTION). This means it needs to be handled carefully to be able - // to detect exceptions thrown by the DexPcMovedEvent itself. These exceptions could be thrown by - // jvmti-agents while handling breakpoint or single step events. We had to move this into its own - // function because it was making ExecuteSwitchImpl have too large a stack. - NO_INLINE static bool DoDexPcMoveEvent(Thread* self, - const CodeItemDataAccessor& accessor, - const ShadowFrame& shadow_frame, - uint32_t dex_pc_, - const instrumentation::Instrumentation* instrumentation, - JValue* save_ref) - REQUIRES_SHARED(Locks::mutator_lock_) { - DCHECK(instrumentation->HasDexPcListeners()); - StackHandleScope<2> hs(self); - Handle<mirror::Throwable> thr(hs.NewHandle(self->GetException())); - mirror::Object* null_obj = nullptr; - HandleWrapper<mirror::Object> h( - hs.NewHandleWrapper(LIKELY(save_ref == nullptr) ? &null_obj : save_ref->GetGCRoot())); - self->ClearException(); - instrumentation->DexPcMovedEvent(self, - shadow_frame.GetThisObject(accessor.InsSize()), - shadow_frame.GetMethod(), - dex_pc_); - if (UNLIKELY(self->IsExceptionPending())) { - // We got a new exception in the dex-pc-moved event. - // We just let this exception replace the old one. - // TODO It would be good to add the old exception to the - // suppressed exceptions of the new one if possible. - return false; // Pending exception. - } - if (UNLIKELY(!thr.IsNull())) { - self->SetException(thr.Get()); - } - return true; - } - HANDLER_ATTRIBUTES bool HandleReturn(JValue result) { Self()->AllowThreadSuspension(); if (!DoMonitorCheckOnExit(Self(), &shadow_frame_)) { return false; } - if (UNLIKELY(NeedsMethodExitEvent(Instrumentation()) && - !SendMethodExitEvents(Self(), - Instrumentation(), - shadow_frame_, - shadow_frame_.GetMethod(), - result))) { + if (UNLIKELY(InstrumentationHandler::NeedsMethodExitEvent(Instrumentation()) && + !InstrumentationHandler::SendMethodExitEvents(Self(), + Instrumentation(), + shadow_frame_, + shadow_frame_.GetMethod(), + result))) { DCHECK(Self()->IsExceptionPending()); // Do not raise exception event if it is caused by other instrumentation event. shadow_frame_.SetSkipNextExceptionEvent(true); @@ -231,8 +407,9 @@ class InstructionHandler { if (UNLIKELY(Self()->ObserveAsyncException())) { return false; // Pending exception. } - if (UNLIKELY(Instrumentation()->HasBranchListeners())) { - Instrumentation()->Branch(Self(), shadow_frame_.GetMethod(), DexPC(), offset); + if (UNLIKELY(InstrumentationHandler::HasBranchListeners(Instrumentation()))) { + InstrumentationHandler::Branch( + Self(), shadow_frame_.GetMethod(), DexPC(), offset, Instrumentation()); } if (!transaction_active) { // TODO: Do OSR only on back-edges and check if OSR code is ready here. @@ -346,13 +523,13 @@ class InstructionHandler { template<FindFieldType find_type, Primitive::Type field_type> HANDLER_ATTRIBUTES bool HandleGet() { return DoFieldGet<find_type, field_type, transaction_active>( - Self(), shadow_frame_, inst_, inst_data_); + Self(), shadow_frame_, inst_, inst_data_, Instrumentation()); } template<FindFieldType find_type, Primitive::Type field_type> HANDLER_ATTRIBUTES bool HandlePut() { return DoFieldPut<find_type, field_type, transaction_active>( - Self(), shadow_frame_, inst_, inst_data_); + Self(), shadow_frame_, inst_, inst_data_, Instrumentation()); } template<InvokeType type, bool is_range> @@ -486,14 +663,14 @@ class InstructionHandler { } } result.SetL(obj_result); - if (UNLIKELY(NeedsMethodExitEvent(Instrumentation()))) { + if (UNLIKELY(InstrumentationHandler::NeedsMethodExitEvent(Instrumentation()))) { StackHandleScope<1> hs(Self()); MutableHandle<mirror::Object> h_result(hs.NewHandle(obj_result)); - if (!SendMethodExitEvents(Self(), - Instrumentation(), - shadow_frame_, - shadow_frame_.GetMethod(), - h_result)) { + if (!InstrumentationHandler::SendMethodExitEvents(Self(), + Instrumentation(), + shadow_frame_, + shadow_frame_.GetMethod(), + h_result)) { DCHECK(Self()->IsExceptionPending()); // Do not raise exception event if it is caused by other instrumentation event. shadow_frame_.SetSkipNextExceptionEvent(true); @@ -679,8 +856,13 @@ class InstructionHandler { gc::AllocatorType allocator_type = Runtime::Current()->GetHeap()->GetCurrentAllocator(); if (UNLIKELY(c->IsStringClass())) { obj = mirror::String::AllocEmptyString(Self(), allocator_type); + // Do not record the allocated string in the transaction. + // There can be no transaction records for this immutable object. } else { obj = AllocObjectFromCode(c, Self(), allocator_type); + if (obj != nullptr) { + TransactionChecker::RecordNewObject(obj); + } } } if (UNLIKELY(obj == nullptr)) { @@ -693,27 +875,26 @@ class InstructionHandler { HANDLER_ATTRIBUTES bool NEW_ARRAY() { int32_t length = GetVReg(B()); - ObjPtr<mirror::Object> obj = AllocArrayFromCode( + ObjPtr<mirror::Array> array = AllocArrayFromCode( dex::TypeIndex(C()), length, shadow_frame_.GetMethod(), Self(), Runtime::Current()->GetHeap()->GetCurrentAllocator()); - if (UNLIKELY(obj == nullptr)) { + if (UNLIKELY(array == nullptr)) { return false; // Pending exception. } - SetVRegReference(A(), obj); + TransactionChecker::RecordNewArray(array); + SetVRegReference(A(), array); return true; } HANDLER_ATTRIBUTES bool FILLED_NEW_ARRAY() { - return DoFilledNewArray<false, transaction_active>( - inst_, shadow_frame_, Self(), ResultRegister()); + return DoFilledNewArray</*is_range=*/ false>(inst_, shadow_frame_, Self(), ResultRegister()); } HANDLER_ATTRIBUTES bool FILLED_NEW_ARRAY_RANGE() { - return DoFilledNewArray<true, transaction_active>( - inst_, shadow_frame_, Self(), ResultRegister()); + return DoFilledNewArray</*is_range=*/ true>(inst_, shadow_frame_, Self(), ResultRegister()); } HANDLER_ATTRIBUTES bool FILL_ARRAY_DATA() { @@ -721,12 +902,11 @@ class InstructionHandler { const Instruction::ArrayDataPayload* payload = reinterpret_cast<const Instruction::ArrayDataPayload*>(payload_addr); ObjPtr<mirror::Object> obj = GetVRegReference(A()); + // If we have an active transaction, record old values before we overwrite them. + TransactionChecker::RecordArrayElementsInTransaction(obj, payload->element_count); if (!FillArrayData(obj, payload)) { return false; // Pending exception. } - if (transaction_active) { - RecordArrayElementsInTransaction(obj->AsArray(), payload->element_count); - } return true; } diff --git a/runtime/interpreter/interpreter_switch_impl0.cc b/runtime/interpreter/interpreter_switch_impl0.cc index 65ae2fe333..1bea9da838 100644 --- a/runtime/interpreter/interpreter_switch_impl0.cc +++ b/runtime/interpreter/interpreter_switch_impl0.cc @@ -22,6 +22,147 @@ namespace art HIDDEN { namespace interpreter { +// Define the helper class that does not do any transaction checks. +class InactiveTransactionChecker { + public: + ALWAYS_INLINE static bool WriteConstraint([[maybe_unused]] Thread* self, + [[maybe_unused]] ObjPtr<mirror::Object> obj) + REQUIRES_SHARED(Locks::mutator_lock_) { + return false; + } + + ALWAYS_INLINE static bool WriteValueConstraint([[maybe_unused]] Thread* self, + [[maybe_unused]] ObjPtr<mirror::Object> value) + REQUIRES_SHARED(Locks::mutator_lock_) { + return false; + } + + ALWAYS_INLINE static bool ReadConstraint([[maybe_unused]] Thread* self, + [[maybe_unused]] ObjPtr<mirror::Object> value) + REQUIRES_SHARED(Locks::mutator_lock_) { + return false; + } + + ALWAYS_INLINE static bool AllocationConstraint([[maybe_unused]] Thread* self, + [[maybe_unused]] ObjPtr<mirror::Class> klass) + REQUIRES_SHARED(Locks::mutator_lock_) { + return false; + } + + ALWAYS_INLINE static bool IsTransactionAborted() { + return false; + } + + static void RecordArrayElementsInTransaction([[maybe_unused]] ObjPtr<mirror::Object> array, + [[maybe_unused]] int32_t count) + REQUIRES_SHARED(Locks::mutator_lock_) {} + + ALWAYS_INLINE static void RecordNewObject([[maybe_unused]] ObjPtr<mirror::Object> new_object) + REQUIRES_SHARED(Locks::mutator_lock_) {} + + ALWAYS_INLINE static void RecordNewArray([[maybe_unused]] ObjPtr<mirror::Array> new_array) + REQUIRES_SHARED(Locks::mutator_lock_) {} +}; + +class ActiveInstrumentationHandler { + public: + ALWAYS_INLINE WARN_UNUSED + static bool HasFieldReadListeners(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + return instrumentation->HasFieldReadListeners(); + } + + ALWAYS_INLINE WARN_UNUSED + static bool HasFieldWriteListeners(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + return instrumentation->HasFieldWriteListeners(); + } + + ALWAYS_INLINE WARN_UNUSED + static bool HasBranchListeners(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + return instrumentation->HasBranchListeners(); + } + + ALWAYS_INLINE WARN_UNUSED + static bool NeedsDexPcEvents(ShadowFrame& shadow_frame) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK_IMPLIES(shadow_frame.GetNotifyDexPcMoveEvents(), + Runtime::Current()->GetInstrumentation()->HasDexPcListeners()); + return shadow_frame.GetNotifyDexPcMoveEvents(); + } + + ALWAYS_INLINE WARN_UNUSED + static bool NeedsMethodExitEvent(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + return interpreter::NeedsMethodExitEvent(instrumentation); + } + + ALWAYS_INLINE WARN_UNUSED + static bool GetForcePopFrame(ShadowFrame& shadow_frame) { + DCHECK_IMPLIES(shadow_frame.GetForcePopFrame(), + Runtime::Current()->AreNonStandardExitsEnabled()); + return shadow_frame.GetForcePopFrame(); + } + + ALWAYS_INLINE + static void Branch(Thread* self, + ArtMethod* method, + uint32_t dex_pc, + int32_t dex_pc_offset, + const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + instrumentation->Branch(self, method, dex_pc, dex_pc_offset); + } + + // Unlike most other events the DexPcMovedEvent can be sent when there is a pending exception (if + // the next instruction is MOVE_EXCEPTION). This means it needs to be handled carefully to be able + // to detect exceptions thrown by the DexPcMovedEvent itself. These exceptions could be thrown by + // jvmti-agents while handling breakpoint or single step events. We had to move this into its own + // function because it was making ExecuteSwitchImpl have too large a stack. + NO_INLINE static bool DoDexPcMoveEvent(Thread* self, + const CodeItemDataAccessor& accessor, + const ShadowFrame& shadow_frame, + uint32_t dex_pc, + const instrumentation::Instrumentation* instrumentation, + JValue* save_ref) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(instrumentation->HasDexPcListeners()); + StackHandleScope<2> hs(self); + Handle<mirror::Throwable> thr(hs.NewHandle(self->GetException())); + mirror::Object* null_obj = nullptr; + HandleWrapper<mirror::Object> h( + hs.NewHandleWrapper(LIKELY(save_ref == nullptr) ? &null_obj : save_ref->GetGCRoot())); + self->ClearException(); + instrumentation->DexPcMovedEvent(self, + shadow_frame.GetThisObject(accessor.InsSize()), + shadow_frame.GetMethod(), + dex_pc); + if (UNLIKELY(self->IsExceptionPending())) { + // We got a new exception in the dex-pc-moved event. + // We just let this exception replace the old one. + // TODO It would be good to add the old exception to the + // suppressed exceptions of the new one if possible. + return false; // Pending exception. + } + if (UNLIKELY(!thr.IsNull())) { + self->SetException(thr.Get()); + } + return true; + } + + template <typename T> + ALWAYS_INLINE WARN_UNUSED + static bool SendMethodExitEvents( + Thread* self, + const instrumentation::Instrumentation* instrumentation, + ShadowFrame& frame, + ArtMethod* method, + T& result) REQUIRES_SHARED(Locks::mutator_lock_) { + return interpreter::SendMethodExitEvents(self, instrumentation, frame, method, result); + } +}; + // Explicit definition of ExecuteSwitchImplCpp. template HOT_ATTR void ExecuteSwitchImplCpp<false>(SwitchImplContext* ctx); diff --git a/runtime/interpreter/interpreter_switch_impl1.cc b/runtime/interpreter/interpreter_switch_impl1.cc index e56c58c2bc..9c5b5b3266 100644 --- a/runtime/interpreter/interpreter_switch_impl1.cc +++ b/runtime/interpreter/interpreter_switch_impl1.cc @@ -19,11 +19,197 @@ #include "interpreter_switch_impl-inl.h" -#include "active_transaction_checker.h" +#include "oat/aot_class_linker.h" +#include "transaction.h" namespace art HIDDEN { namespace interpreter { +class ActiveTransactionChecker { + public: + static inline bool WriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) + REQUIRES_SHARED(Locks::mutator_lock_) { + return GetClassLinker()->TransactionWriteConstraint(self, obj); + } + + static inline bool WriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) + REQUIRES_SHARED(Locks::mutator_lock_) { + return GetClassLinker()->TransactionWriteValueConstraint(self, value); + } + + static inline bool ReadConstraint(Thread* self, ObjPtr<mirror::Object> obj) + REQUIRES_SHARED(Locks::mutator_lock_) { + return GetClassLinker()->TransactionReadConstraint(self, obj); + } + + static inline bool AllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) + REQUIRES_SHARED(Locks::mutator_lock_) { + return GetClassLinker()->TransactionAllocationConstraint(self, klass); + } + + static inline bool IsTransactionAborted() { + return GetClassLinker()->IsTransactionAborted(); + } + + static void RecordArrayElementsInTransaction(ObjPtr<mirror::Object> array, int32_t count) + REQUIRES_SHARED(Locks::mutator_lock_); + + static void RecordNewObject(ObjPtr<mirror::Object> new_object) + REQUIRES_SHARED(Locks::mutator_lock_) { + GetClassLinker()->GetTransaction()->RecordNewObject(new_object); + } + + static void RecordNewArray(ObjPtr<mirror::Array> new_object) + REQUIRES_SHARED(Locks::mutator_lock_) { + GetClassLinker()->GetTransaction()->RecordNewArray(new_object); + } + + private: + static AotClassLinker* GetClassLinker() { + return down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + } +}; + +// TODO: Use ObjPtr here. +template<typename T> +static void RecordArrayElementsInTransactionImpl(Transaction* transaction, + ObjPtr<mirror::PrimitiveArray<T>> array, + int32_t count) + REQUIRES_SHARED(Locks::mutator_lock_) { + for (int32_t i = 0; i < count; ++i) { + transaction->RecordWriteArray(array.Ptr(), i, array->GetWithoutChecks(i)); + } +} + +void ActiveTransactionChecker::RecordArrayElementsInTransaction(ObjPtr<mirror::Object> array, + int32_t count) { + DCHECK(Runtime::Current()->IsActiveTransaction()); + if (array == nullptr) { + return; // The interpreter shall throw NPE. + } + DCHECK(array->IsArrayInstance()); + DCHECK_LE(count, array->AsArray()->GetLength()); + Transaction* transaction = GetClassLinker()->GetTransaction(); + if (!transaction->ArrayNeedsTransactionRecords(array->AsArray())) { + return; + } + // No read barrier is needed for reading a chain of constant references + // for reading a constant primitive value, see `ReadBarrierOption`. + Primitive::Type primitive_component_type = + array->GetClass<kDefaultVerifyFlags, kWithoutReadBarrier>() + ->GetComponentType<kDefaultVerifyFlags, kWithoutReadBarrier>()->GetPrimitiveType(); + switch (primitive_component_type) { + case Primitive::kPrimBoolean: + RecordArrayElementsInTransactionImpl(transaction, array->AsBooleanArray(), count); + break; + case Primitive::kPrimByte: + RecordArrayElementsInTransactionImpl(transaction, array->AsByteArray(), count); + break; + case Primitive::kPrimChar: + RecordArrayElementsInTransactionImpl(transaction, array->AsCharArray(), count); + break; + case Primitive::kPrimShort: + RecordArrayElementsInTransactionImpl(transaction, array->AsShortArray(), count); + break; + case Primitive::kPrimInt: + RecordArrayElementsInTransactionImpl(transaction, array->AsIntArray(), count); + break; + case Primitive::kPrimFloat: + RecordArrayElementsInTransactionImpl(transaction, array->AsFloatArray(), count); + break; + case Primitive::kPrimLong: + RecordArrayElementsInTransactionImpl(transaction, array->AsLongArray(), count); + break; + case Primitive::kPrimDouble: + RecordArrayElementsInTransactionImpl(transaction, array->AsDoubleArray(), count); + break; + default: + LOG(FATAL) << "Unsupported primitive type " << primitive_component_type + << " in fill-array-data"; + UNREACHABLE(); + } +} + +class InactiveInstrumentationHandler { + public: + ALWAYS_INLINE WARN_UNUSED + static bool HasFieldReadListeners(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(!instrumentation->HasFieldReadListeners()); + return false; + } + + ALWAYS_INLINE WARN_UNUSED + static bool HasFieldWriteListeners(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(!instrumentation->HasFieldWriteListeners()); + return false; + } + + ALWAYS_INLINE WARN_UNUSED + static bool HasBranchListeners(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(!instrumentation->HasBranchListeners()); + return false; + } + + ALWAYS_INLINE WARN_UNUSED + static bool NeedsDexPcEvents(ShadowFrame& shadow_frame) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(!shadow_frame.GetNotifyDexPcMoveEvents()); + DCHECK(!Runtime::Current()->GetInstrumentation()->HasDexPcListeners()); + return false; + } + + ALWAYS_INLINE WARN_UNUSED + static bool NeedsMethodExitEvent(const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(!interpreter::NeedsMethodExitEvent(instrumentation)); + return false; + } + + ALWAYS_INLINE WARN_UNUSED + static bool GetForcePopFrame(ShadowFrame& shadow_frame) { + DCHECK(!shadow_frame.GetForcePopFrame()); + DCHECK(!Runtime::Current()->AreNonStandardExitsEnabled()); + return false; + } + + NO_RETURN + static void Branch([[maybe_unused]] Thread* self, + [[maybe_unused]] ArtMethod* method, + [[maybe_unused]] uint32_t dex_pc, + [[maybe_unused]] int32_t dex_pc_offset, + [[maybe_unused]] const instrumentation::Instrumentation* instrumentation) + REQUIRES_SHARED(Locks::mutator_lock_) { + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); + } + + static bool DoDexPcMoveEvent( + [[maybe_unused]] Thread* self, + [[maybe_unused]] const CodeItemDataAccessor& accessor, + [[maybe_unused]] const ShadowFrame& shadow_frame, + [[maybe_unused]] uint32_t dex_pc, + [[maybe_unused]] const instrumentation::Instrumentation* instrumentation, + [[maybe_unused]] JValue* save_ref) + REQUIRES_SHARED(Locks::mutator_lock_) { + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); + } + + template <typename T> + static bool SendMethodExitEvents( + [[maybe_unused]] Thread* self, + [[maybe_unused]] const instrumentation::Instrumentation* instrumentation, + [[maybe_unused]] ShadowFrame& frame, + [[maybe_unused]] ArtMethod* method, + [[maybe_unused]] T& result) REQUIRES_SHARED(Locks::mutator_lock_) { + LOG(FATAL) << "UNREACHABLE"; + UNREACHABLE(); + } +}; + // Explicit definition of ExecuteSwitchImplCpp. template void ExecuteSwitchImplCpp<true>(SwitchImplContext* ctx); diff --git a/runtime/interpreter/mterp/arm64ng/object.S b/runtime/interpreter/mterp/arm64ng/object.S index 5202ebcd3d..ade39d5e89 100644 --- a/runtime/interpreter/mterp/arm64ng/object.S +++ b/runtime/interpreter/mterp/arm64ng/object.S @@ -195,6 +195,10 @@ mov x3, #0 EXPORT_PC bl nterp_get_instance_field_offset + // Zero extension (nterp_get_instance_field_offset returns uint32_t) of the return value is + // needed as the value is used below via wider X0 register - AARCH64 AAPCS specifies that + // "... any unused bits in the register have unspecified value" (see 6.8.2, 6.9). + mov w0, w0 tbz w0, #31, .L${opcode}_resume CLEAR_INSTANCE_VOLATILE_MARKER w0 lsr w2, wINST, #12 // w2<- B @@ -273,6 +277,10 @@ .endif EXPORT_PC bl nterp_get_instance_field_offset + // Zero extension (nterp_get_instance_field_offset returns uint32_t) of the return value is + // needed as the value is used below via wider X0 register - AARCH64 AAPCS specifies that + // "... any unused bits in the register have unspecified value" (see 6.8.2, 6.9). + mov w0, w0 .if $is_object // Reload the value as it may have moved. ubfx w1, wINST, #8, #4 // w1<- A diff --git a/runtime/interpreter/mterp/nterp.cc b/runtime/interpreter/mterp/nterp.cc index d8ce2bcaed..64f04d6fde 100644 --- a/runtime/interpreter/mterp/nterp.cc +++ b/runtime/interpreter/mterp/nterp.cc @@ -693,7 +693,10 @@ extern "C" jit::OsrData* NterpHotMethod(ArtMethod* method, uint16_t* dex_pc_ptr, DCHECK_EQ(Thread::Current()->GetSharedMethodHotness(), 0u); Thread::Current()->ResetSharedMethodHotness(); } else { + // Move the counter to the initial threshold in case we have to re-JIT it. method->ResetCounter(runtime->GetJITOptions()->GetWarmupThreshold()); + // Mark the method as warm for the profile saver. + method->SetPreviouslyWarm(); } jit::Jit* jit = runtime->GetJit(); if (jit != nullptr && jit->UseJitCompilation()) { diff --git a/runtime/interpreter/unstarted_runtime.cc b/runtime/interpreter/unstarted_runtime.cc index d865327473..43e0692eee 100644 --- a/runtime/interpreter/unstarted_runtime.cc +++ b/runtime/interpreter/unstarted_runtime.cc @@ -80,7 +80,7 @@ static void AbortTransactionOrFail(Thread* self, const char* fmt, ...) { Runtime* runtime = Runtime::Current(); if (runtime->IsActiveTransaction()) { va_start(args, fmt); - runtime->AbortTransactionV(self, fmt, args); + runtime->GetClassLinker()->AbortTransactionV(self, fmt, args); va_end(args); } else { va_start(args, fmt); @@ -99,7 +99,7 @@ static void CharacterLowerUpper(Thread* self, JValue* result, size_t arg_offset, bool to_lower_case) REQUIRES_SHARED(Locks::mutator_lock_) { - uint32_t int_value = static_cast<uint32_t>(shadow_frame->GetVReg(arg_offset)); + int32_t int_value = shadow_frame->GetVReg(arg_offset); // Only ASCII (7-bit). if (!isascii(int_value)) { @@ -109,14 +109,16 @@ static void CharacterLowerUpper(Thread* self, return; } - std::locale c_locale("C"); - char char_value = static_cast<char>(int_value); - - if (to_lower_case) { - result->SetI(std::tolower(char_value, c_locale)); - } else { - result->SetI(std::toupper(char_value, c_locale)); - } + // Constructing a `std::locale("C")` is slow. Use an explicit calculation, compare in debug mode. + int32_t masked_value = int_value & ~0x20; // Clear bit distinguishing `A`..`Z` from `a`..`z`. + bool is_ascii_letter = ('A' <= masked_value) && (masked_value <= 'Z'); + int32_t result_value = is_ascii_letter ? (masked_value | (to_lower_case ? 0x20 : 0)) : int_value; + DCHECK_EQ(result_value, + to_lower_case + ? std::tolower(dchecked_integral_cast<char>(int_value), std::locale("C")) + : std::toupper(dchecked_integral_cast<char>(int_value), std::locale("C"))) + << std::boolalpha << to_lower_case; + result->SetI(result_value); } void UnstartedRuntime::UnstartedCharacterToLowerCase( @@ -156,6 +158,12 @@ static void UnstartedRuntimeFindClass(Thread* self, result->SetL(found); } +static inline bool PendingExceptionHasAbortDescriptor(Thread* self) + REQUIRES_SHARED(Locks::mutator_lock_) { + DCHECK(self->IsExceptionPending()); + return self->GetException()->GetClass()->DescriptorEquals(kTransactionAbortErrorDescriptor); +} + // Common helper for class-loading cutouts in an unstarted runtime. We call Runtime methods that // rely on Java code to wrap errors in the correct exception class (i.e., NoClassDefFoundError into // ClassNotFoundException), so need to do the same. The only exception is if the exception is @@ -165,21 +173,22 @@ static void CheckExceptionGenerateClassNotFound(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_) { if (self->IsExceptionPending()) { Runtime* runtime = Runtime::Current(); - DCHECK_EQ(runtime->IsTransactionAborted(), - self->GetException()->GetClass()->DescriptorEquals(kTransactionAbortErrorDescriptor)) - << self->GetException()->GetClass()->PrettyDescriptor(); if (runtime->IsActiveTransaction()) { // The boot class path at run time may contain additional dex files with // the required class definition(s). We cannot throw a normal exception at // compile time because a class initializer could catch it and successfully // initialize a class differently than when executing at run time. // If we're not aborting the transaction yet, abort now. b/183691501 - if (!runtime->IsTransactionAborted()) { - runtime->AbortTransactionF(self, "ClassNotFoundException"); + if (!runtime->GetClassLinker()->IsTransactionAborted()) { + DCHECK(!PendingExceptionHasAbortDescriptor(self)); + runtime->GetClassLinker()->AbortTransactionF(self, "ClassNotFoundException"); + } else { + DCHECK(PendingExceptionHasAbortDescriptor(self)) + << self->GetException()->GetClass()->PrettyDescriptor(); } } else { // If not in a transaction, it cannot be the transaction abort exception. Wrap it. - DCHECK(!runtime->IsTransactionAborted()); + DCHECK(!PendingExceptionHasAbortDescriptor(self)); self->ThrowNewWrappedException("Ljava/lang/ClassNotFoundException;", "ClassNotFoundException"); } @@ -761,18 +770,19 @@ void UnstartedRuntime::UnstartedVmClassLoaderFindLoadedClass( // This might have an error pending. But semantics are to just return null. if (self->IsExceptionPending()) { Runtime* runtime = Runtime::Current(); - DCHECK_EQ(runtime->IsTransactionAborted(), - self->GetException()->GetClass()->DescriptorEquals(kTransactionAbortErrorDescriptor)) - << self->GetException()->GetClass()->PrettyDescriptor(); if (runtime->IsActiveTransaction()) { // If we're not aborting the transaction yet, abort now. b/183691501 // See CheckExceptionGenerateClassNotFound() for more detailed explanation. - if (!runtime->IsTransactionAborted()) { - runtime->AbortTransactionF(self, "ClassNotFoundException"); + if (!runtime->GetClassLinker()->IsTransactionAborted()) { + DCHECK(!PendingExceptionHasAbortDescriptor(self)); + runtime->GetClassLinker()->AbortTransactionF(self, "ClassNotFoundException"); + } else { + DCHECK(PendingExceptionHasAbortDescriptor(self)) + << self->GetException()->GetClass()->PrettyDescriptor(); } } else { // If not in a transaction, it cannot be the transaction abort exception. Clear it. - DCHECK(!runtime->IsTransactionAborted()); + DCHECK(!PendingExceptionHasAbortDescriptor(self)); self->ClearException(); } } @@ -1300,7 +1310,7 @@ static void UnstartedMemoryPeekArray( int64_t address_long = shadow_frame->GetVRegLong(arg_offset); mirror::Object* obj = shadow_frame->GetVRegReference(arg_offset + 2); if (obj == nullptr) { - Runtime::Current()->AbortTransactionAndThrowAbortError(self, "Null pointer in peekArray"); + Runtime::Current()->GetClassLinker()->AbortTransactionF(self, "Null pointer in peekArray"); return; } ObjPtr<mirror::Array> array = obj->AsArray(); @@ -1308,9 +1318,8 @@ static void UnstartedMemoryPeekArray( int offset = shadow_frame->GetVReg(arg_offset + 3); int count = shadow_frame->GetVReg(arg_offset + 4); if (offset < 0 || offset + count > array->GetLength()) { - std::string error_msg(StringPrintf("Array out of bounds in peekArray: %d/%d vs %d", - offset, count, array->GetLength())); - Runtime::Current()->AbortTransactionAndThrowAbortError(self, error_msg); + Runtime::Current()->GetClassLinker()->AbortTransactionF( + self, "Array out of bounds in peekArray: %d/%d vs %d", offset, count, array->GetLength()); return; } @@ -2432,9 +2441,10 @@ void UnstartedRuntime::Jni(Thread* self, ArtMethod* method, mirror::Object* rece } else { Runtime* runtime = Runtime::Current(); if (runtime->IsActiveTransaction()) { - runtime->AbortTransactionF(self, - "Attempt to invoke native method in non-started runtime: %s", - ArtMethod::PrettyMethod(method).c_str()); + runtime->GetClassLinker()->AbortTransactionF( + self, + "Attempt to invoke native method in non-started runtime: %s", + ArtMethod::PrettyMethod(method).c_str()); } else { LOG(FATAL) << "Calling native method " << ArtMethod::PrettyMethod(method) << " in an unstarted non-transactional runtime"; diff --git a/runtime/interpreter/unstarted_runtime.h b/runtime/interpreter/unstarted_runtime.h index 60092ad958..8019c257d4 100644 --- a/runtime/interpreter/unstarted_runtime.h +++ b/runtime/interpreter/unstarted_runtime.h @@ -99,7 +99,7 @@ class UnstartedRuntime { static void InitializeInvokeHandlers(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_); static void InitializeJNIHandlers(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_); - friend class UnstartedRuntimeTest; + friend class UnstartedRuntimeTestBase; DISALLOW_ALLOCATION(); DISALLOW_COPY_AND_ASSIGN(UnstartedRuntime); diff --git a/runtime/interpreter/unstarted_runtime_test.cc b/runtime/interpreter/unstarted_runtime_test.cc index 44c1e2627a..cf7782dcdc 100644 --- a/runtime/interpreter/unstarted_runtime_test.cc +++ b/runtime/interpreter/unstarted_runtime_test.cc @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "unstarted_runtime.h" +#include "unstarted_runtime_test.h" #include <limits> #include <locale> @@ -30,7 +30,7 @@ #include "dex/dex_instruction.h" #include "handle.h" #include "handle_scope-inl.h" -#include "interpreter/interpreter_common.h" +#include "interpreter_common.h" #include "mirror/array-alloc-inl.h" #include "mirror/class-alloc-inl.h" #include "mirror/class_loader.h" @@ -48,55 +48,8 @@ namespace art HIDDEN { namespace interpreter { -// Deleter to be used with ShadowFrame::CreateDeoptimizedFrame objects. -struct DeoptShadowFrameDelete { - // NOTE: Deleting a const object is valid but free() takes a non-const pointer. - void operator()(ShadowFrame* ptr) const { - if (ptr != nullptr) { - ShadowFrame::DeleteDeoptimizedFrame(ptr); - } - } -}; -// Alias for std::unique_ptr<> that uses the above deleter. -using UniqueDeoptShadowFramePtr = std::unique_ptr<ShadowFrame, DeoptShadowFrameDelete>; - -class UnstartedRuntimeTest : public CommonRuntimeTest { +class UnstartedRuntimeTest : public UnstartedRuntimeTestBase { protected: - // Re-expose all UnstartedRuntime implementations so we don't need to declare a million - // test friends. - - // Methods that intercept available libcore implementations. -#define UNSTARTED_DIRECT(Name, DescriptorIgnored, NameIgnored, SignatureIgnored) \ - static void Unstarted ## Name(Thread* self, \ - ShadowFrame* shadow_frame, \ - JValue* result, \ - size_t arg_offset) \ - REQUIRES_SHARED(Locks::mutator_lock_) { \ - interpreter::UnstartedRuntime::Unstarted ## Name(self, shadow_frame, result, arg_offset); \ - } - UNSTARTED_RUNTIME_DIRECT_LIST(UNSTARTED_DIRECT) -#undef UNSTARTED_DIRECT - - // Methods that are native. -#define UNSTARTED_JNI(Name, DescriptorIgnored, NameIgnored, SignatureIgnored) \ - static void UnstartedJNI ## Name(Thread* self, \ - ArtMethod* method, \ - mirror::Object* receiver, \ - uint32_t* args, \ - JValue* result) \ - REQUIRES_SHARED(Locks::mutator_lock_) { \ - interpreter::UnstartedRuntime::UnstartedJNI ## Name(self, method, receiver, args, result); \ - } - UNSTARTED_RUNTIME_JNI_LIST(UNSTARTED_JNI) -#undef UNSTARTED_JNI - - UniqueDeoptShadowFramePtr CreateShadowFrame(uint32_t num_vregs, - ArtMethod* method, - uint32_t dex_pc) { - return UniqueDeoptShadowFramePtr( - ShadowFrame::CreateDeoptimizedFrame(num_vregs, method, dex_pc)); - } - // Helpers for ArrayCopy. // // Note: as we have to use handles, we use StackHandleScope to transfer data. Hardcode a size @@ -210,16 +163,6 @@ class UnstartedRuntimeTest : public CommonRuntimeTest { EXPECT_EQ(expect_int64t, result_int64t) << result.GetD() << " vs " << test_pairs[i][1]; } } - - // Prepare for aborts. Aborts assume that the exception class is already resolved, as the - // loading code doesn't work under transactions. - void PrepareForAborts() REQUIRES_SHARED(Locks::mutator_lock_) { - ObjPtr<mirror::Object> result = Runtime::Current()->GetClassLinker()->FindClass( - Thread::Current(), - kTransactionAbortErrorDescriptor, - ScopedNullHandle<mirror::ClassLoader>()); - CHECK(result != nullptr); - } }; TEST_F(UnstartedRuntimeTest, MemoryPeekByte) { @@ -396,8 +339,7 @@ TEST_F(UnstartedRuntimeTest, StringInit) { ScopedObjectAccess soa(self); ObjPtr<mirror::Class> klass = GetClassRoot<mirror::String>(); ArtMethod* method = - klass->FindConstructor("(Ljava/lang/String;)V", - Runtime::Current()->GetClassLinker()->GetImagePointerSize()); + klass->FindConstructor("(Ljava/lang/String;)V", class_linker_->GetImagePointerSize()); ASSERT_TRUE(method != nullptr); // create instruction data for invoke-direct {v0, v1} of method with fake index @@ -483,9 +425,6 @@ TEST_F(UnstartedRuntimeTest, SystemArrayCopyObjectArrayTest) { JValue result; UniqueDeoptShadowFramePtr tmp = CreateShadowFrame(10, nullptr, 0); - StackHandleScope<1> hs_object(self); - Handle<mirror::Class> object_class(hs_object.NewHandle(GetClassRoot<mirror::Object>())); - // Simple test: // [1,2,3]{1 @ 2} into [4,5,6] = [4,2,6] { @@ -507,8 +446,8 @@ TEST_F(UnstartedRuntimeTest, SystemArrayCopyObjectArrayTest) { RunArrayCopy(self, tmp.get(), false, - object_class.Get(), - object_class.Get(), + GetClassRoot<mirror::Object>(), + GetClassRoot<mirror::Object>(), hs_src, 1, hs_dst, @@ -538,7 +477,7 @@ TEST_F(UnstartedRuntimeTest, SystemArrayCopyObjectArrayTest) { RunArrayCopy(self, tmp.get(), false, - object_class.Get(), + GetClassRoot<mirror::Object>(), GetClassRoot<mirror::String>(), hs_src, 1, @@ -569,7 +508,7 @@ TEST_F(UnstartedRuntimeTest, SystemArrayCopyObjectArrayTest) { RunArrayCopy(self, tmp.get(), true, - object_class.Get(), + GetClassRoot<mirror::Object>(), GetClassRoot<mirror::String>(), hs_src, 0, @@ -773,51 +712,6 @@ TEST_F(UnstartedRuntimeTest, ToLowerUpper) { } } } - - // Check abort for other things. Can't test all. - - PrepareForAborts(); - - for (uint32_t i = 128; i < 256; ++i) { - { - JValue result; - tmp->SetVReg(0, static_cast<int32_t>(i)); - EnterTransactionMode(); - UnstartedCharacterToLowerCase(self, tmp.get(), &result, 0); - ASSERT_TRUE(IsTransactionAborted()); - ExitTransactionMode(); - ASSERT_TRUE(self->IsExceptionPending()); - } - { - JValue result; - tmp->SetVReg(0, static_cast<int32_t>(i)); - EnterTransactionMode(); - UnstartedCharacterToUpperCase(self, tmp.get(), &result, 0); - ASSERT_TRUE(IsTransactionAborted()); - ExitTransactionMode(); - ASSERT_TRUE(self->IsExceptionPending()); - } - } - for (uint64_t i = 256; i <= std::numeric_limits<uint32_t>::max(); i <<= 1) { - { - JValue result; - tmp->SetVReg(0, static_cast<int32_t>(i)); - EnterTransactionMode(); - UnstartedCharacterToLowerCase(self, tmp.get(), &result, 0); - ASSERT_TRUE(IsTransactionAborted()); - ExitTransactionMode(); - ASSERT_TRUE(self->IsExceptionPending()); - } - { - JValue result; - tmp->SetVReg(0, static_cast<int32_t>(i)); - EnterTransactionMode(); - UnstartedCharacterToUpperCase(self, tmp.get(), &result, 0); - ASSERT_TRUE(IsTransactionAborted()); - ExitTransactionMode(); - ASSERT_TRUE(self->IsExceptionPending()); - } - } } TEST_F(UnstartedRuntimeTest, Sin) { @@ -937,21 +831,20 @@ TEST_F(UnstartedRuntimeTest, ThreadLocalGet) { UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); StackHandleScope<1> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); // Positive test. See that We get something for float conversion. { Handle<mirror::Class> floating_decimal = hs.NewHandle( - class_linker->FindClass(self, - "Ljdk/internal/math/FloatingDecimal;", - ScopedNullHandle<mirror::ClassLoader>())); + class_linker_->FindClass(self, + "Ljdk/internal/math/FloatingDecimal;", + ScopedNullHandle<mirror::ClassLoader>())); ASSERT_TRUE(floating_decimal != nullptr); - ASSERT_TRUE(class_linker->EnsureInitialized(self, floating_decimal, true, true)); + ASSERT_TRUE(class_linker_->EnsureInitialized(self, floating_decimal, true, true)); ArtMethod* caller_method = floating_decimal->FindClassMethod( "getBinaryToASCIIBuffer", "()Ljdk/internal/math/FloatingDecimal$BinaryToASCIIBuffer;", - class_linker->GetImagePointerSize()); + class_linker_->GetImagePointerSize()); // floating_decimal->DumpClass(LOG_STREAM(ERROR), mirror::Class::kDumpClassFullDetail); ASSERT_TRUE(caller_method != nullptr); ASSERT_TRUE(caller_method->IsDirect()); @@ -965,27 +858,6 @@ TEST_F(UnstartedRuntimeTest, ThreadLocalGet) { shadow_frame->ClearLink(); } - - // Negative test. - PrepareForAborts(); - - { - // Just use a method in Class. - ObjPtr<mirror::Class> class_class = GetClassRoot<mirror::Class>(); - ArtMethod* caller_method = - &*class_class->GetDeclaredMethods(class_linker->GetImagePointerSize()).begin(); - UniqueDeoptShadowFramePtr caller_frame = CreateShadowFrame(10, caller_method, 0); - shadow_frame->SetLink(caller_frame.get()); - - EnterTransactionMode(); - UnstartedThreadLocalGet(self, shadow_frame.get(), &result, 0); - ASSERT_TRUE(IsTransactionAborted()); - ExitTransactionMode(); - ASSERT_TRUE(self->IsExceptionPending()); - self->ClearException(); - - shadow_frame->ClearLink(); - } } TEST_F(UnstartedRuntimeTest, FloatConversion) { @@ -993,17 +865,16 @@ TEST_F(UnstartedRuntimeTest, FloatConversion) { ScopedObjectAccess soa(self); StackHandleScope<1> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); Handle<mirror::Class> double_class = hs.NewHandle( - class_linker->FindClass(self, - "Ljava/lang/Double;", - ScopedNullHandle<mirror::ClassLoader>())); + class_linker_->FindClass(self, + "Ljava/lang/Double;", + ScopedNullHandle<mirror::ClassLoader>())); ASSERT_TRUE(double_class != nullptr); - ASSERT_TRUE(class_linker->EnsureInitialized(self, double_class, true, true)); + ASSERT_TRUE(class_linker_->EnsureInitialized(self, double_class, true, true)); ArtMethod* method = double_class->FindClassMethod("toString", "(D)Ljava/lang/String;", - class_linker->GetImagePointerSize()); + class_linker_->GetImagePointerSize()); ASSERT_TRUE(method != nullptr); ASSERT_TRUE(method->IsDirect()); ASSERT_TRUE(method->GetDeclaringClass() == double_class.Get()); @@ -1029,62 +900,26 @@ TEST_F(UnstartedRuntimeTest, FloatConversion) { EXPECT_EQ("1.23", mod_utf); } -TEST_F(UnstartedRuntimeTest, ThreadCurrentThread) { - Thread* self = Thread::Current(); - ScopedObjectAccess soa(self); - - JValue result; - UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); - - StackHandleScope<1> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); - Handle<mirror::Class> thread_class = hs.NewHandle( - class_linker->FindClass(self, "Ljava/lang/Thread;", ScopedNullHandle<mirror::ClassLoader>())); - ASSERT_TRUE(thread_class.Get() != nullptr); - ASSERT_TRUE(class_linker->EnsureInitialized(self, thread_class, true, true)); - - // Negative test. In general, currentThread should fail (as we should not leak a peer that will - // be recreated at runtime). - PrepareForAborts(); - - { - EnterTransactionMode(); - UnstartedThreadCurrentThread(self, shadow_frame.get(), &result, 0); - ASSERT_TRUE(IsTransactionAborted()); - ExitTransactionMode(); - ASSERT_TRUE(self->IsExceptionPending()); - self->ClearException(); - } -} - TEST_F(UnstartedRuntimeTest, LogManager) { Thread* self = Thread::Current(); ScopedObjectAccess soa(self); StackHandleScope<1> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); - Handle<mirror::Class> log_manager_class = hs.NewHandle( - class_linker->FindClass(self, - "Ljava/util/logging/LogManager;", - ScopedNullHandle<mirror::ClassLoader>())); + Handle<mirror::Class> log_manager_class = hs.NewHandle(class_linker_->FindClass( + self, "Ljava/util/logging/LogManager;", ScopedNullHandle<mirror::ClassLoader>())); ASSERT_TRUE(log_manager_class.Get() != nullptr); - ASSERT_TRUE(class_linker->EnsureInitialized(self, log_manager_class, true, true)); + ASSERT_TRUE(class_linker_->EnsureInitialized(self, log_manager_class, true, true)); } class UnstartedClassForNameTest : public UnstartedRuntimeTest { public: template <typename T> - void RunTest(T& runner, bool in_transaction, bool should_succeed) { + void RunTest(T&& runner) { Thread* self = Thread::Current(); ScopedObjectAccess soa(self); // Ensure that Class is initialized. - { - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); - StackHandleScope<1> hs(self); - Handle<mirror::Class> h_class = hs.NewHandle(GetClassRoot<mirror::Class>()); - CHECK(class_linker->EnsureInitialized(self, h_class, true, true)); - } + CHECK(GetClassRoot<mirror::Class>()->IsInitialized()); // A selection of classes from different core classpath components. constexpr const char* kTestCases[] = { @@ -1092,99 +927,20 @@ class UnstartedClassForNameTest : public UnstartedRuntimeTest { "dalvik.system.ClassExt", // From libart. }; - if (in_transaction) { - // For transaction mode, we cannot load any classes, as the pre-fence initialization of - // classes isn't transactional. Load them ahead of time. - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); - for (const char* name : kTestCases) { - class_linker->FindClass(self, - DotToDescriptor(name).c_str(), - ScopedNullHandle<mirror::ClassLoader>()); - CHECK(!self->IsExceptionPending()) << self->GetException()->Dump(); - } - } - - if (!should_succeed) { - // Negative test. In general, currentThread should fail (as we should not leak a peer that will - // be recreated at runtime). - PrepareForAborts(); - } - JValue result; UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); for (const char* name : kTestCases) { ObjPtr<mirror::String> name_string = mirror::String::AllocFromModifiedUtf8(self, name); CHECK(name_string != nullptr); - - if (in_transaction) { - StackHandleScope<1> hs(self); - HandleWrapperObjPtr<mirror::String> h(hs.NewHandleWrapper(&name_string)); - EnterTransactionMode(); - } CHECK(!self->IsExceptionPending()); runner(self, shadow_frame.get(), name_string, &result); - if (should_succeed) { - CHECK(!self->IsExceptionPending()) << name << " " << self->GetException()->Dump(); - CHECK(result.GetL() != nullptr) << name; - } else { - CHECK(self->IsExceptionPending()) << name; - if (in_transaction) { - ASSERT_TRUE(IsTransactionAborted()); - } - self->ClearException(); - } - - if (in_transaction) { - ExitTransactionMode(); - } + CHECK(!self->IsExceptionPending()) << name << " " << self->GetException()->Dump(); + CHECK(result.GetL() != nullptr) << name; } } - - mirror::ClassLoader* GetBootClassLoader() REQUIRES_SHARED(Locks::mutator_lock_) { - Thread* self = Thread::Current(); - StackHandleScope<2> hs(self); - MutableHandle<mirror::ClassLoader> boot_cp = hs.NewHandle<mirror::ClassLoader>(nullptr); - - { - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); - - // Create the fake boot classloader. Any instance is fine, they are technically interchangeable. - Handle<mirror::Class> boot_cp_class = hs.NewHandle( - class_linker->FindClass(self, - "Ljava/lang/BootClassLoader;", - ScopedNullHandle<mirror::ClassLoader>())); - CHECK(boot_cp_class != nullptr); - CHECK(class_linker->EnsureInitialized(self, boot_cp_class, true, true)); - - boot_cp.Assign(boot_cp_class->AllocObject(self)->AsClassLoader()); - CHECK(boot_cp != nullptr); - - ArtMethod* boot_cp_init = boot_cp_class->FindConstructor( - "()V", class_linker->GetImagePointerSize()); - CHECK(boot_cp_init != nullptr); - - JValue result; - UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, boot_cp_init, 0); - shadow_frame->SetVRegReference(0, boot_cp.Get()); - - // create instruction data for invoke-direct {v0} of method with fake index - uint16_t inst_data[3] = { 0x1070, 0x0000, 0x0010 }; - - interpreter::DoCall<false>(boot_cp_init, - self, - *shadow_frame, - Instruction::At(inst_data), - inst_data[0], - /* string_init= */ false, - &result); - CHECK(!self->IsExceptionPending()); - } - - return boot_cp.Get(); - } }; TEST_F(UnstartedClassForNameTest, ClassForName) { @@ -1195,7 +951,7 @@ TEST_F(UnstartedClassForNameTest, ClassForName) { shadow_frame->SetVRegReference(0, name); UnstartedClassForName(self, shadow_frame, result, 0); }; - RunTest(runner, false, true); + RunTest(runner); } TEST_F(UnstartedClassForNameTest, ClassForNameLong) { @@ -1208,7 +964,7 @@ TEST_F(UnstartedClassForNameTest, ClassForNameLong) { shadow_frame->SetVRegReference(2, nullptr); UnstartedClassForNameLong(self, shadow_frame, result, 0); }; - RunTest(runner, false, true); + RunTest(runner); } TEST_F(UnstartedClassForNameTest, ClassForNameLongWithClassLoader) { @@ -1227,50 +983,7 @@ TEST_F(UnstartedClassForNameTest, ClassForNameLongWithClassLoader) { shadow_frame->SetVRegReference(2, boot_cp.Get()); UnstartedClassForNameLong(th, shadow_frame, result, 0); }; - RunTest(runner, false, true); -} - -TEST_F(UnstartedClassForNameTest, ClassForNameLongWithClassLoaderTransaction) { - Thread* self = Thread::Current(); - ScopedObjectAccess soa(self); - - StackHandleScope<1> hs(self); - Handle<mirror::ClassLoader> boot_cp = hs.NewHandle(GetBootClassLoader()); - - auto runner = [&](Thread* th, - ShadowFrame* shadow_frame, - ObjPtr<mirror::String> name, - JValue* result) - REQUIRES_SHARED(Locks::mutator_lock_) { - shadow_frame->SetVRegReference(0, name); - shadow_frame->SetVReg(1, 0); - shadow_frame->SetVRegReference(2, boot_cp.Get()); - UnstartedClassForNameLong(th, shadow_frame, result, 0); - }; - RunTest(runner, true, true); -} - -TEST_F(UnstartedClassForNameTest, ClassForNameLongWithClassLoaderFail) { - Thread* self = Thread::Current(); - ScopedObjectAccess soa(self); - - StackHandleScope<2> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); - jobject path_jobj = class_linker->CreatePathClassLoader(self, {}); - ASSERT_TRUE(path_jobj != nullptr); - Handle<mirror::ClassLoader> path_cp = hs.NewHandle<mirror::ClassLoader>( - self->DecodeJObject(path_jobj)->AsClassLoader()); - - auto runner = [&](Thread* th, - ShadowFrame* shadow_frame, - ObjPtr<mirror::String> name, - JValue* result) REQUIRES_SHARED(Locks::mutator_lock_) { - shadow_frame->SetVRegReference(0, name); - shadow_frame->SetVReg(1, 0); - shadow_frame->SetVRegReference(2, path_cp.Get()); - UnstartedClassForNameLong(th, shadow_frame, result, 0); - }; - RunTest(runner, true, false); + RunTest(runner); } TEST_F(UnstartedRuntimeTest, ClassGetSignatureAnnotation) { @@ -1278,13 +991,12 @@ TEST_F(UnstartedRuntimeTest, ClassGetSignatureAnnotation) { ScopedObjectAccess soa(self); StackHandleScope<1> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); Handle<mirror::Class> list_class = hs.NewHandle( - class_linker->FindClass(self, - "Ljava/util/List;", - ScopedNullHandle<mirror::ClassLoader>())); + class_linker_->FindClass(self, + "Ljava/util/List;", + ScopedNullHandle<mirror::ClassLoader>())); ASSERT_TRUE(list_class.Get() != nullptr); - ASSERT_TRUE(class_linker->EnsureInitialized(self, list_class, true, true)); + ASSERT_TRUE(class_linker_->EnsureInitialized(self, list_class, true, true)); JValue result; UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); @@ -1315,17 +1027,16 @@ TEST_F(UnstartedRuntimeTest, ConstructorNewInstance0) { ScopedObjectAccess soa(self); StackHandleScope<4> hs(self); - ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); // Get Throwable. Handle<mirror::Class> throw_class = hs.NewHandle(GetClassRoot<mirror::Throwable>()); - ASSERT_TRUE(class_linker->EnsureInitialized(self, throw_class, true, true)); + ASSERT_TRUE(class_linker_->EnsureInitialized(self, throw_class, true, true)); // Get an input object. Handle<mirror::String> input = hs.NewHandle(mirror::String::AllocFromModifiedUtf8(self, "abd")); // Find the constructor. - PointerSize pointer_size = class_linker->GetImagePointerSize(); + PointerSize pointer_size = class_linker_->GetImagePointerSize(); ArtMethod* throw_cons = throw_class->FindConstructor("(Ljava/lang/String;)V", pointer_size); ASSERT_TRUE(throw_cons != nullptr); Handle<mirror::Constructor> cons = hs.NewHandle((pointer_size == PointerSize::k64) diff --git a/runtime/interpreter/unstarted_runtime_test.h b/runtime/interpreter/unstarted_runtime_test.h new file mode 100644 index 0000000000..465a4268ea --- /dev/null +++ b/runtime/interpreter/unstarted_runtime_test.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_RUNTIME_INTERPRETER_UNSTARTED_RUNTIME_TEST_H_ +#define ART_RUNTIME_INTERPRETER_UNSTARTED_RUNTIME_TEST_H_ + +#include "unstarted_runtime.h" + +#include <memory> + +#include "class_linker-inl.h" +#include "common_runtime_test.h" +#include "handle.h" +#include "handle_scope-inl.h" +#include "interpreter_common.h" +#include "mirror/object_array-alloc-inl.h" +#include "mirror/object_array-inl.h" +#include "shadow_frame-inl.h" + +namespace art HIDDEN { +namespace interpreter { + +// Deleter to be used with ShadowFrame::CreateDeoptimizedFrame objects. +struct DeoptShadowFrameDelete { + // NOTE: Deleting a const object is valid but free() takes a non-const pointer. + void operator()(ShadowFrame* ptr) const { + ShadowFrame::DeleteDeoptimizedFrame(ptr); + } +}; +// Alias for std::unique_ptr<> that uses the above deleter. +using UniqueDeoptShadowFramePtr = std::unique_ptr<ShadowFrame, DeoptShadowFrameDelete>; + +class UnstartedRuntimeTestBase : public CommonRuntimeTest { + protected: + // Re-expose all UnstartedRuntime implementations so we don't need to declare a million + // test friends. + + // Methods that intercept available libcore implementations. +#define UNSTARTED_DIRECT(Name, DescriptorIgnored, NameIgnored, SignatureIgnored) \ + static void Unstarted ## Name(Thread* self, \ + ShadowFrame* shadow_frame, \ + JValue* result, \ + size_t arg_offset) \ + REQUIRES_SHARED(Locks::mutator_lock_) { \ + interpreter::UnstartedRuntime::Unstarted ## Name(self, shadow_frame, result, arg_offset); \ + } + UNSTARTED_RUNTIME_DIRECT_LIST(UNSTARTED_DIRECT) +#undef UNSTARTED_DIRECT + + // Methods that are native. +#define UNSTARTED_JNI(Name, DescriptorIgnored, NameIgnored, SignatureIgnored) \ + static void UnstartedJNI ## Name(Thread* self, \ + ArtMethod* method, \ + mirror::Object* receiver, \ + uint32_t* args, \ + JValue* result) \ + REQUIRES_SHARED(Locks::mutator_lock_) { \ + interpreter::UnstartedRuntime::UnstartedJNI ## Name(self, method, receiver, args, result); \ + } + UNSTARTED_RUNTIME_JNI_LIST(UNSTARTED_JNI) +#undef UNSTARTED_JNI + + UniqueDeoptShadowFramePtr CreateShadowFrame(uint32_t num_vregs, + ArtMethod* method, + uint32_t dex_pc) { + return UniqueDeoptShadowFramePtr( + ShadowFrame::CreateDeoptimizedFrame(num_vregs, method, dex_pc)); + } + + mirror::ClassLoader* GetBootClassLoader() REQUIRES_SHARED(Locks::mutator_lock_) { + Thread* self = Thread::Current(); + StackHandleScope<2> hs(self); + + // Create the fake boot classloader. Any instance is fine, they are technically interchangeable. + Handle<mirror::Class> boot_cp_class = hs.NewHandle(class_linker_->FindClass( + self, "Ljava/lang/BootClassLoader;", ScopedNullHandle<mirror::ClassLoader>())); + CHECK(boot_cp_class != nullptr); + CHECK(class_linker_->EnsureInitialized( + self, boot_cp_class, /*can_init_fields=*/ true, /*can_init_parents=*/ true)); + + Handle<mirror::ClassLoader> boot_cp = + hs.NewHandle(boot_cp_class->AllocObject(self)->AsClassLoader()); + CHECK(boot_cp != nullptr); + + ArtMethod* boot_cp_init = + boot_cp_class->FindConstructor("()V", class_linker_->GetImagePointerSize()); + CHECK(boot_cp_init != nullptr); + + JValue result; + UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, boot_cp_init, 0); + shadow_frame->SetVRegReference(0, boot_cp.Get()); + + // create instruction data for invoke-direct {v0} of method with fake index + uint16_t inst_data[3] = { 0x1070, 0x0000, 0x0010 }; + + interpreter::DoCall<false>(boot_cp_init, + self, + *shadow_frame, + Instruction::At(inst_data), + inst_data[0], + /* string_init= */ false, + &result); + CHECK(!self->IsExceptionPending()); + + return boot_cp.Get(); + } +}; + +} // namespace interpreter +} // namespace art + +#endif // ART_RUNTIME_INTERPRETER_UNSTARTED_RUNTIME_TEST_H_ diff --git a/runtime/interpreter/unstarted_runtime_transaction_test.cc b/runtime/interpreter/unstarted_runtime_transaction_test.cc new file mode 100644 index 0000000000..abbcbbb12e --- /dev/null +++ b/runtime/interpreter/unstarted_runtime_transaction_test.cc @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "unstarted_runtime_test.h" + +#include "class_root-inl.h" +#include "common_transaction_test.h" +#include "dex/descriptors_names.h" +#include "interpreter/interpreter_common.h" +#include "handle.h" +#include "handle_scope-inl.h" +#include "mirror/class-inl.h" + +namespace art HIDDEN { +namespace interpreter { + +class UnstartedRuntimeTransactionTest : public CommonTransactionTestBase<UnstartedRuntimeTestBase> { + protected: + // Prepare for aborts. Aborts assume that the exception class is already resolved, as the + // loading code doesn't work under transactions. + void PrepareForAborts() REQUIRES_SHARED(Locks::mutator_lock_) { + ObjPtr<mirror::Object> result = Runtime::Current()->GetClassLinker()->FindClass( + Thread::Current(), + kTransactionAbortErrorDescriptor, + ScopedNullHandle<mirror::ClassLoader>()); + CHECK(result != nullptr); + } +}; + +TEST_F(UnstartedRuntimeTransactionTest, ToLowerUpper) { + Thread* self = Thread::Current(); + ScopedObjectAccess soa(self); + UniqueDeoptShadowFramePtr tmp = CreateShadowFrame(10, nullptr, 0); + + PrepareForAborts(); + + for (uint32_t i = 128; i < 256; ++i) { + { + JValue result; + tmp->SetVReg(0, static_cast<int32_t>(i)); + EnterTransactionMode(); + UnstartedCharacterToLowerCase(self, tmp.get(), &result, 0); + ASSERT_TRUE(IsTransactionAborted()); + ExitTransactionMode(); + ASSERT_TRUE(self->IsExceptionPending()); + } + { + JValue result; + tmp->SetVReg(0, static_cast<int32_t>(i)); + EnterTransactionMode(); + UnstartedCharacterToUpperCase(self, tmp.get(), &result, 0); + ASSERT_TRUE(IsTransactionAborted()); + ExitTransactionMode(); + ASSERT_TRUE(self->IsExceptionPending()); + } + } + for (uint64_t i = 256; i <= std::numeric_limits<uint32_t>::max(); i <<= 1) { + { + JValue result; + tmp->SetVReg(0, static_cast<int32_t>(i)); + EnterTransactionMode(); + UnstartedCharacterToLowerCase(self, tmp.get(), &result, 0); + ASSERT_TRUE(IsTransactionAborted()); + ExitTransactionMode(); + ASSERT_TRUE(self->IsExceptionPending()); + } + { + JValue result; + tmp->SetVReg(0, static_cast<int32_t>(i)); + EnterTransactionMode(); + UnstartedCharacterToUpperCase(self, tmp.get(), &result, 0); + ASSERT_TRUE(IsTransactionAborted()); + ExitTransactionMode(); + ASSERT_TRUE(self->IsExceptionPending()); + } + } +} + +TEST_F(UnstartedRuntimeTransactionTest, ThreadLocalGet) { + Thread* self = Thread::Current(); + ScopedObjectAccess soa(self); + UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); + + // Negative test. + PrepareForAborts(); + + // Just use a method in Class. + ObjPtr<mirror::Class> class_class = GetClassRoot<mirror::Class>(); + ArtMethod* caller_method = + &*class_class->GetDeclaredMethods(class_linker_->GetImagePointerSize()).begin(); + UniqueDeoptShadowFramePtr caller_frame = CreateShadowFrame(10, caller_method, 0); + shadow_frame->SetLink(caller_frame.get()); + + JValue result; + EnterTransactionMode(); + UnstartedThreadLocalGet(self, shadow_frame.get(), &result, 0); + ASSERT_TRUE(IsTransactionAborted()); + ExitTransactionMode(); + ASSERT_TRUE(self->IsExceptionPending()); + self->ClearException(); + + shadow_frame->ClearLink(); +} + +TEST_F(UnstartedRuntimeTransactionTest, ThreadCurrentThread) { + Thread* self = Thread::Current(); + ScopedObjectAccess soa(self); + UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); + + // Negative test. In general, currentThread should fail (as we should not leak a peer that will + // be recreated at runtime). + PrepareForAborts(); + + JValue result; + EnterTransactionMode(); + UnstartedThreadCurrentThread(self, shadow_frame.get(), &result, 0); + ASSERT_TRUE(IsTransactionAborted()); + ExitTransactionMode(); + ASSERT_TRUE(self->IsExceptionPending()); + self->ClearException(); +} + +class UnstartedClassForNameTransactionTest : public UnstartedRuntimeTransactionTest { + public: + template <typename T> + void RunTest(T&& runner, bool should_succeed) { + Thread* self = Thread::Current(); + ScopedObjectAccess soa(self); + + // Ensure that Class is initialized. + CHECK(GetClassRoot<mirror::Class>()->IsInitialized()); + + // A selection of classes from different core classpath components. + constexpr const char* kTestCases[] = { + "java.net.CookieManager", // From libcore. + "dalvik.system.ClassExt", // From libart. + }; + + // For transaction mode, we cannot load any classes, as the pre-fence initialization of + // classes isn't transactional. Load them ahead of time. + for (const char* name : kTestCases) { + class_linker_->FindClass(self, + DotToDescriptor(name).c_str(), + ScopedNullHandle<mirror::ClassLoader>()); + CHECK(!self->IsExceptionPending()) << self->GetException()->Dump(); + } + + if (!should_succeed) { + // Negative test. In general, currentThread should fail (as we should not leak a peer that will + // be recreated at runtime). + PrepareForAborts(); + } + + JValue result; + UniqueDeoptShadowFramePtr shadow_frame = CreateShadowFrame(10, nullptr, 0); + + for (const char* name : kTestCases) { + EnterTransactionMode(); + + ObjPtr<mirror::String> name_string = mirror::String::AllocFromModifiedUtf8(self, name); + CHECK(name_string != nullptr); + CHECK(!self->IsExceptionPending()); + + runner(self, shadow_frame.get(), name_string, &result); + + if (should_succeed) { + CHECK(!self->IsExceptionPending()) << name << " " << self->GetException()->Dump(); + CHECK(result.GetL() != nullptr) << name; + } else { + CHECK(self->IsExceptionPending()) << name; + ASSERT_TRUE(IsTransactionAborted()); + self->ClearException(); + } + + ExitTransactionMode(); + } + } +}; + +TEST_F(UnstartedClassForNameTransactionTest, ClassForNameLongWithClassLoader) { + Thread* self = Thread::Current(); + ScopedObjectAccess soa(self); + + StackHandleScope<1> hs(self); + Handle<mirror::ClassLoader> boot_cp = hs.NewHandle(GetBootClassLoader()); + + auto runner = [&](Thread* th, + ShadowFrame* shadow_frame, + ObjPtr<mirror::String> name, + JValue* result) + REQUIRES_SHARED(Locks::mutator_lock_) { + shadow_frame->SetVRegReference(0, name); + shadow_frame->SetVReg(1, 0); + shadow_frame->SetVRegReference(2, boot_cp.Get()); + UnstartedClassForNameLong(th, shadow_frame, result, 0); + }; + RunTest(runner, /*should_succeed=*/ true); +} + +TEST_F(UnstartedClassForNameTransactionTest, ClassForNameLongWithClassLoaderFail) { + Thread* self = Thread::Current(); + ScopedObjectAccess soa(self); + + StackHandleScope<2> hs(self); + jobject path_jobj = class_linker_->CreatePathClassLoader(self, {}); + ASSERT_TRUE(path_jobj != nullptr); + Handle<mirror::ClassLoader> path_cp = hs.NewHandle<mirror::ClassLoader>( + self->DecodeJObject(path_jobj)->AsClassLoader()); + + auto runner = [&](Thread* th, + ShadowFrame* shadow_frame, + ObjPtr<mirror::String> name, + JValue* result) REQUIRES_SHARED(Locks::mutator_lock_) { + shadow_frame->SetVRegReference(0, name); + shadow_frame->SetVReg(1, 0); + shadow_frame->SetVRegReference(2, path_cp.Get()); + UnstartedClassForNameLong(th, shadow_frame, result, 0); + }; + RunTest(runner, /*should_succeed=*/ false); +} + +} // namespace interpreter +} // namespace art diff --git a/runtime/jit/jit.cc b/runtime/jit/jit.cc index cb80b4151d..4d395c8528 100644 --- a/runtime/jit/jit.cc +++ b/runtime/jit/jit.cc @@ -17,6 +17,7 @@ #include "jit.h" #include <dlfcn.h> +#include <sys/resource.h> #include "art_method-inl.h" #include "base/file_utils.h" @@ -1487,6 +1488,13 @@ static void* RunPollingThread(void* arg) { /* create_peer= */ false); CHECK(thread_attached); + if (getpriority(PRIO_PROCESS, 0 /* this thread */) == 0) { + // Slightly reduce thread priority, mostly so the suspend logic notices that we're + // not a high priority thread, and can time out more slowly. May fail on host. + (void)setpriority(PRIO_PROCESS, 0 /* this thread */, 1); + } else { + PLOG(ERROR) << "Unexpected BootImagePollingThread priority: " << getpriority(PRIO_PROCESS, 0); + } { // Prevent other threads from running while we are remapping the boot image // ArtMethod's. Native threads might still be running, but they cannot diff --git a/runtime/jit/jit_code_cache.cc b/runtime/jit/jit_code_cache.cc index eae56a45b3..4b69dc5c01 100644 --- a/runtime/jit/jit_code_cache.cc +++ b/runtime/jit/jit_code_cache.cc @@ -342,6 +342,8 @@ const void* JitCodeCache::GetSavedEntryPointOfPreCompiledMethod(ArtMethod* metho auto it = saved_compiled_methods_map_.find(method); if (it != saved_compiled_methods_map_.end()) { code_ptr = it->second; + // Now that we're using the saved entrypoint, remove it from the saved map. + saved_compiled_methods_map_.erase(it); } } if (code_ptr != nullptr) { @@ -488,33 +490,36 @@ void JitCodeCache::FreeAllMethodHeaders( ->RemoveDependentsWithMethodHeaders(method_headers); } - ScopedCodeCacheWrite scc(private_region_); - for (const OatQuickMethodHeader* method_header : method_headers) { - FreeCodeAndData(method_header->GetCode()); - } + { + ScopedCodeCacheWrite scc(private_region_); + for (const OatQuickMethodHeader* method_header : method_headers) { + FreeCodeAndData(method_header->GetCode()); + } - // We have potentially removed a lot of debug info. Do maintenance pass to save space. - RepackNativeDebugInfoForJit(); + // We have potentially removed a lot of debug info. Do maintenance pass to save space. + RepackNativeDebugInfoForJit(); + } // Check that the set of compiled methods exactly matches native debug information. // Does not check zygote methods since they can change concurrently. if (kIsDebugBuild && !Runtime::Current()->IsZygote()) { std::map<const void*, ArtMethod*> compiled_methods; + std::set<const void*> debug_info; VisitAllMethods([&](const void* addr, ArtMethod* method) { if (!IsInZygoteExecSpace(addr)) { CHECK(addr != nullptr && method != nullptr); compiled_methods.emplace(addr, method); } }); - std::set<const void*> debug_info; ForEachNativeDebugSymbol([&](const void* addr, size_t, const char* name) { addr = AlignDown(addr, GetInstructionSetInstructionAlignment(kRuntimeISA)); // Thumb-bit. - CHECK(debug_info.emplace(addr).second) << "Duplicate debug info: " << addr << " " << name; + bool res = debug_info.emplace(addr).second; + CHECK(res) << "Duplicate debug info: " << addr << " " << name; CHECK_EQ(compiled_methods.count(addr), 1u) << "Extra debug info: " << addr << " " << name; }); if (!debug_info.empty()) { // If debug-info generation is enabled. for (const auto& [addr, method] : compiled_methods) { - CHECK_EQ(debug_info.count(addr), 1u) << "No debug info: " << method->PrettyMethod(); + CHECK_EQ(debug_info.count(addr), 1u) << "Mising debug info"; } CHECK_EQ(compiled_methods.size(), debug_info.size()); } @@ -529,52 +534,67 @@ void JitCodeCache::RemoveMethodsIn(Thread* self, const LinearAlloc& alloc) { // for entries in this set. And it's more efficient to iterate through // the CHA dependency map just once with an unordered_set. std::unordered_set<OatQuickMethodHeader*> method_headers; + MutexLock mu(self, *Locks::jit_lock_); + // We do not check if a code cache GC is in progress, as this method comes + // with the classlinker_classes_lock_ held, and suspending ourselves could + // lead to a deadlock. { - MutexLock mu(self, *Locks::jit_lock_); - // We do not check if a code cache GC is in progress, as this method comes - // with the classlinker_classes_lock_ held, and suspending ourselves could - // lead to a deadlock. - { - for (auto it = jni_stubs_map_.begin(); it != jni_stubs_map_.end();) { - it->second.RemoveMethodsIn(alloc); - if (it->second.GetMethods().empty()) { - method_headers.insert(OatQuickMethodHeader::FromCodePointer(it->second.GetCode())); - it = jni_stubs_map_.erase(it); - } else { - it->first.UpdateShorty(it->second.GetMethods().front()); - ++it; - } + for (auto it = jni_stubs_map_.begin(); it != jni_stubs_map_.end();) { + it->second.RemoveMethodsIn(alloc); + if (it->second.GetMethods().empty()) { + method_headers.insert(OatQuickMethodHeader::FromCodePointer(it->second.GetCode())); + it = jni_stubs_map_.erase(it); + } else { + it->first.UpdateShorty(it->second.GetMethods().front()); + ++it; } - for (auto it = method_code_map_.begin(); it != method_code_map_.end();) { - if (alloc.ContainsUnsafe(it->second)) { - method_headers.insert(OatQuickMethodHeader::FromCodePointer(it->first)); - VLOG(jit) << "JIT removed " << it->second->PrettyMethod() << ": " << it->first; - it = method_code_map_.erase(it); - } else { - ++it; - } + } + for (auto it = zombie_jni_code_.begin(); it != zombie_jni_code_.end();) { + if (alloc.ContainsUnsafe(*it)) { + it = zombie_jni_code_.erase(it); + } else { + ++it; } } - for (auto it = osr_code_map_.begin(); it != osr_code_map_.end();) { - if (alloc.ContainsUnsafe(it->first)) { - // Note that the code has already been pushed to method_headers in the loop - // above and is going to be removed in FreeCode() below. - it = osr_code_map_.erase(it); + for (auto it = processed_zombie_jni_code_.begin(); it != processed_zombie_jni_code_.end();) { + if (alloc.ContainsUnsafe(*it)) { + it = processed_zombie_jni_code_.erase(it); } else { ++it; } } - for (auto it = profiling_infos_.begin(); it != profiling_infos_.end();) { - ProfilingInfo* info = it->second; - if (alloc.ContainsUnsafe(info->GetMethod())) { - private_region_.FreeWritableData(reinterpret_cast<uint8_t*>(info)); - it = profiling_infos_.erase(it); + for (auto it = method_code_map_.begin(); it != method_code_map_.end();) { + if (alloc.ContainsUnsafe(it->second)) { + method_headers.insert(OatQuickMethodHeader::FromCodePointer(it->first)); + VLOG(jit) << "JIT removed " << it->second->PrettyMethod() << ": " << it->first; + zombie_code_.erase(it->first); + processed_zombie_code_.erase(it->first); + it = method_code_map_.erase(it); } else { ++it; } } - FreeAllMethodHeaders(method_headers); } + for (auto it = osr_code_map_.begin(); it != osr_code_map_.end();) { + DCHECK(!ContainsElement(zombie_code_, it->second)); + if (alloc.ContainsUnsafe(it->first)) { + // Note that the code has already been pushed to method_headers in the loop + // above and is going to be removed in FreeCode() below. + it = osr_code_map_.erase(it); + } else { + ++it; + } + } + for (auto it = profiling_infos_.begin(); it != profiling_infos_.end();) { + ProfilingInfo* info = it->second; + if (alloc.ContainsUnsafe(info->GetMethod())) { + private_region_.FreeWritableData(reinterpret_cast<uint8_t*>(info)); + it = profiling_infos_.erase(it); + } else { + ++it; + } + } + FreeAllMethodHeaders(method_headers); } bool JitCodeCache::IsWeakAccessEnabled(Thread* self) const { @@ -629,29 +649,6 @@ void JitCodeCache::CopyInlineCacheInto( } } -static void ClearMethodCounter(ArtMethod* method, bool was_warm) - REQUIRES_SHARED(Locks::mutator_lock_) { - if (was_warm) { - method->SetPreviouslyWarm(); - } - method->ResetCounter(Runtime::Current()->GetJITOptions()->GetWarmupThreshold()); - // We add one sample so that the profile knows that the method was executed at least once. - // This is required for layout purposes. - method->UpdateCounter(/* new_samples= */ 1); -} - -void JitCodeCache::WaitForPotentialCollectionToCompleteRunnable(Thread* self) { - while (collection_in_progress_) { - Locks::jit_lock_->Unlock(self); - { - ScopedThreadSuspension sts(self, ThreadState::kSuspended); - MutexLock mu(self, *Locks::jit_lock_); - WaitForPotentialCollectionToComplete(self); - } - Locks::jit_lock_->Lock(self); - } -} - bool JitCodeCache::Commit(Thread* self, JitMemoryRegion* region, ArtMethod* method, @@ -679,9 +676,6 @@ bool JitCodeCache::Commit(Thread* self, OatQuickMethodHeader* method_header = nullptr; { MutexLock mu(self, *Locks::jit_lock_); - // We need to make sure that there will be no jit-gcs going on and wait for any ongoing one to - // finish. - WaitForPotentialCollectionToCompleteRunnable(self); const uint8_t* code_ptr = region->CommitCode(reserved_code, code, stack_map_data); if (code_ptr == nullptr) { return false; @@ -722,10 +716,9 @@ bool JitCodeCache::Commit(Thread* self, bool single_impl_still_valid = true; for (ArtMethod* single_impl : cha_single_implementation_list) { if (!single_impl->HasSingleImplementation()) { - // Simply discard the compiled code. Clear the counter so that it may be recompiled later. + // Simply discard the compiled code. // Hopefully the class hierarchy will be more stable when compilation is retried. single_impl_still_valid = false; - ClearMethodCounter(method, /*was_warm=*/ false); break; } } @@ -781,11 +774,6 @@ bool JitCodeCache::Commit(Thread* self, method, method_header->GetEntryPoint()); } } - if (collection_in_progress_) { - // We need to update the live bitmap if there is a GC to ensure it sees this new - // code. - GetLiveBitmap()->AtomicTestAndSet(FromCodeToAllocation(code_ptr)); - } VLOG(jit) << "JIT added (kind=" << compilation_kind << ") " << ArtMethod::PrettyMethod(method) << "@" << method @@ -825,7 +813,6 @@ bool JitCodeCache::RemoveMethod(ArtMethod* method, bool release_memory) { return false; } - ClearMethodCounter(method, /* was_warm= */ false); Runtime::Current()->GetInstrumentation()->InitializeMethodsCode(method, /*aot_code=*/ nullptr); VLOG(jit) << "JIT removed (osr=" << std::boolalpha << osr << std::noboolalpha << ") " @@ -854,6 +841,7 @@ bool JitCodeCache::RemoveMethodLocked(ArtMethod* method, bool release_memory) { FreeCodeAndData(it->second.GetCode()); } jni_stubs_map_.erase(it); + zombie_jni_code_.erase(method); } else { it->first.UpdateShorty(it->second.GetMethods().front()); } @@ -984,7 +972,6 @@ bool JitCodeCache::Reserve(Thread* self, { ScopedThreadSuspension sts(self, ThreadState::kSuspended); MutexLock mu(self, *Locks::jit_lock_); - WaitForPotentialCollectionToComplete(self); ScopedCodeCacheWrite ccw(*region); code = region->AllocateCode(code_size); data = region->AllocateData(data_size); @@ -1001,8 +988,8 @@ bool JitCodeCache::Reserve(Thread* self, << PrettySize(data_size); return false; } - // Run a code cache collection and try again. - GarbageCollectCache(self); + // Increase the capacity and try again. + IncreaseCodeCacheCapacity(self); } *reserved_code = ArrayRef<const uint8_t>(code, code_size); @@ -1080,11 +1067,6 @@ class MarkCodeClosure final : public Closure { Barrier* const barrier_; }; -void JitCodeCache::NotifyCollectionDone(Thread* self) { - collection_in_progress_ = false; - lock_cond_.Broadcast(self); -} - void JitCodeCache::MarkCompiledCodeOnThreadStacks(Thread* self) { Barrier barrier(0); size_t threads_running_checkpoint = 0; @@ -1102,86 +1084,116 @@ bool JitCodeCache::IsAtMaxCapacity() const { return private_region_.GetCurrentCapacity() == private_region_.GetMaxCapacity(); } -void JitCodeCache::GarbageCollectCache(Thread* self) { +void JitCodeCache::IncreaseCodeCacheCapacity(Thread* self) { + ScopedThreadSuspension sts(self, ThreadState::kSuspended); + MutexLock mu(self, *Locks::jit_lock_); + // Wait for a potential collection, as the size of the bitmap used by that collection + // is of the current capacity. + WaitForPotentialCollectionToComplete(self); + private_region_.IncreaseCodeCacheCapacity(); +} + +void JitCodeCache::RemoveUnmarkedCode(Thread* self) { ScopedTrace trace(__FUNCTION__); - // Wait for an existing collection, or let everyone know we are starting one. - { - ScopedThreadSuspension sts(self, ThreadState::kSuspended); - MutexLock mu(self, *Locks::jit_lock_); - if (!garbage_collect_code_) { - private_region_.IncreaseCodeCacheCapacity(); - return; - } else if (WaitForPotentialCollectionToComplete(self)) { - return; + std::unordered_set<OatQuickMethodHeader*> method_headers; + ScopedDebugDisallowReadBarriers sddrb(self); + MutexLock mu(self, *Locks::jit_lock_); + // Iterate over all zombie code and remove entries that are not marked. + for (auto it = processed_zombie_code_.begin(); it != processed_zombie_code_.end();) { + const void* code_ptr = *it; + uintptr_t allocation = FromCodeToAllocation(code_ptr); + DCHECK(!IsInZygoteExecSpace(code_ptr)); + if (GetLiveBitmap()->Test(allocation)) { + ++it; } else { - number_of_collections_++; - live_bitmap_.reset(CodeCacheBitmap::Create( - "code-cache-bitmap", - reinterpret_cast<uintptr_t>(private_region_.GetExecPages()->Begin()), - reinterpret_cast<uintptr_t>( - private_region_.GetExecPages()->Begin() + private_region_.GetCurrentCapacity() / 2))); - collection_in_progress_ = true; + OatQuickMethodHeader* header = OatQuickMethodHeader::FromCodePointer(code_ptr); + method_headers.insert(header); + method_code_map_.erase(header->GetCode()); + VLOG(jit) << "JIT removed " << *it; + it = processed_zombie_code_.erase(it); + } + } + for (auto it = processed_zombie_jni_code_.begin(); it != processed_zombie_jni_code_.end();) { + ArtMethod* method = *it; + auto stub = jni_stubs_map_.find(JniStubKey(method)); + if (stub == jni_stubs_map_.end()) { + it = processed_zombie_jni_code_.erase(it); + continue; + } + JniStubData& data = stub->second; + if (!data.IsCompiled() || !ContainsElement(data.GetMethods(), method)) { + it = processed_zombie_jni_code_.erase(it); + } else if (method->GetEntryPointFromQuickCompiledCode() == + OatQuickMethodHeader::FromCodePointer(data.GetCode())->GetEntryPoint()) { + // The stub got reused for this method, remove ourselves from the zombie + // list. + it = processed_zombie_jni_code_.erase(it); + } else if (!GetLiveBitmap()->Test(FromCodeToAllocation(data.GetCode()))) { + data.RemoveMethod(method); + if (data.GetMethods().empty()) { + OatQuickMethodHeader* header = OatQuickMethodHeader::FromCodePointer(data.GetCode()); + method_headers.insert(header); + CHECK(ContainsPc(header)); + VLOG(jit) << "JIT removed native code of" << method->PrettyMethod(); + jni_stubs_map_.erase(stub); + } else { + stub->first.UpdateShorty(stub->second.GetMethods().front()); + } + it = processed_zombie_jni_code_.erase(it); + } else { + ++it; } } + FreeAllMethodHeaders(method_headers); +} - TimingLogger logger("JIT code cache timing logger", true, VLOG_IS_ON(jit)); - { - TimingLogger::ScopedTiming st("Code cache collection", &logger); +void JitCodeCache::AddZombieCode(ArtMethod* method, const void* entry_point) { + CHECK(ContainsPc(entry_point)); + CHECK(method->IsNative() || (method->GetEntryPointFromQuickCompiledCode() != entry_point)); + const void* code_ptr = OatQuickMethodHeader::FromEntryPoint(entry_point)->GetCode(); + if (!IsInZygoteExecSpace(code_ptr)) { + Thread* self = Thread::Current(); + if (Locks::jit_lock_->IsExclusiveHeld(self)) { + AddZombieCodeInternal(method, code_ptr); + } else { + MutexLock mu(Thread::Current(), *Locks::jit_lock_); + AddZombieCodeInternal(method, code_ptr); + } + } +} - VLOG(jit) << "Do code cache collection, code=" - << PrettySize(CodeCacheSize()) - << ", data=" << PrettySize(DataCacheSize()); - DoCollection(self); +class JitGcTask final : public Task { + public: + JitGcTask() {} - VLOG(jit) << "After code cache collection, code=" - << PrettySize(CodeCacheSize()) - << ", data=" << PrettySize(DataCacheSize()); + void Run(Thread* self) override { + Runtime::Current()->GetJit()->GetCodeCache()->DoCollection(self); + } - { - MutexLock mu(self, *Locks::jit_lock_); - private_region_.IncreaseCodeCacheCapacity(); - live_bitmap_.reset(nullptr); - NotifyCollectionDone(self); - } + void Finalize() override { + delete this; } - Runtime::Current()->GetJit()->AddTimingLogger(logger); -} +}; -void JitCodeCache::RemoveUnmarkedCode(Thread* self) { - ScopedTrace trace(__FUNCTION__); - ScopedDebugDisallowReadBarriers sddrb(self); - std::unordered_set<OatQuickMethodHeader*> method_headers; - { - MutexLock mu(self, *Locks::jit_lock_); - // Iterate over all compiled code and remove entries that are not marked. - for (auto it = jni_stubs_map_.begin(); it != jni_stubs_map_.end();) { - JniStubData* data = &it->second; - if (IsInZygoteExecSpace(data->GetCode()) || - !data->IsCompiled() || - GetLiveBitmap()->Test(FromCodeToAllocation(data->GetCode()))) { - ++it; - } else { - method_headers.insert(OatQuickMethodHeader::FromCodePointer(data->GetCode())); - for (ArtMethod* method : data->GetMethods()) { - VLOG(jit) << "JIT removed (JNI) " << method->PrettyMethod() << ": " << data->GetCode(); - } - it = jni_stubs_map_.erase(it); - } - } - for (auto it = method_code_map_.begin(); it != method_code_map_.end();) { - const void* code_ptr = it->first; - uintptr_t allocation = FromCodeToAllocation(code_ptr); - if (IsInZygoteExecSpace(code_ptr) || GetLiveBitmap()->Test(allocation)) { - ++it; - } else { - OatQuickMethodHeader* header = OatQuickMethodHeader::FromCodePointer(code_ptr); - method_headers.insert(header); - VLOG(jit) << "JIT removed " << it->second->PrettyMethod() << ": " << it->first; - it = method_code_map_.erase(it); - } +void JitCodeCache::AddZombieCodeInternal(ArtMethod* method, const void* code_ptr) { + if (method->IsNative()) { + CHECK(jni_stubs_map_.find(JniStubKey(method)) != jni_stubs_map_.end()); + zombie_jni_code_.insert(method); + } else { + CHECK(!ContainsElement(zombie_code_, code_ptr)); + zombie_code_.insert(code_ptr); + } + // Arbitrary threshold of number of zombie code before doing a GC. + static constexpr size_t kNumberOfZombieCodeThreshold = kIsDebugBuild ? 1 : 1000; + size_t number_of_code_to_delete = + zombie_code_.size() + zombie_jni_code_.size() + osr_code_map_.size(); + if (number_of_code_to_delete >= kNumberOfZombieCodeThreshold) { + JitThreadPool* pool = Runtime::Current()->GetJit()->GetThreadPool(); + if (pool != nullptr && !gc_task_scheduled_) { + gc_task_scheduled_ = true; + pool->AddTask(Thread::Current(), new JitGcTask()); } - FreeAllMethodHeaders(method_headers); } } @@ -1222,62 +1234,60 @@ void JitCodeCache::MaybeUpdateInlineCache(ArtMethod* method, info->AddInvokeInfo(dex_pc, cls.Ptr()); } -void JitCodeCache::ResetHotnessCounter(ArtMethod* method, Thread* self) { - ScopedDebugDisallowReadBarriers sddrb(self); - MutexLock mu(self, *Locks::jit_lock_); - auto it = profiling_infos_.find(method); - DCHECK(it != profiling_infos_.end()); - it->second->ResetCounter(); -} - - void JitCodeCache::DoCollection(Thread* self) { ScopedTrace trace(__FUNCTION__); + { ScopedDebugDisallowReadBarriers sddrb(self); MutexLock mu(self, *Locks::jit_lock_); - // Mark compiled code that are entrypoints of ArtMethods. Compiled code that is not - // an entry point is either: - // - an osr compiled code, that will be removed if not in a thread call stack. - // - discarded compiled code, that will be removed if not in a thread call stack. - for (const auto& entry : jni_stubs_map_) { - const JniStubData& data = entry.second; - const void* code_ptr = data.GetCode(); - if (IsInZygoteExecSpace(code_ptr)) { - continue; - } - const OatQuickMethodHeader* method_header = OatQuickMethodHeader::FromCodePointer(code_ptr); - for (ArtMethod* method : data.GetMethods()) { - if (method_header->GetEntryPoint() == method->GetEntryPointFromQuickCompiledCode()) { - GetLiveBitmap()->AtomicTestAndSet(FromCodeToAllocation(code_ptr)); - break; - } - } - } - for (const auto& it : method_code_map_) { - ArtMethod* method = it.second; - const void* code_ptr = it.first; - if (IsInZygoteExecSpace(code_ptr)) { - continue; - } - const OatQuickMethodHeader* method_header = OatQuickMethodHeader::FromCodePointer(code_ptr); - if (method_header->GetEntryPoint() == method->GetEntryPointFromQuickCompiledCode()) { - GetLiveBitmap()->AtomicTestAndSet(FromCodeToAllocation(code_ptr)); - } + if (!garbage_collect_code_) { + return; + } else if (WaitForPotentialCollectionToComplete(self)) { + return; } - + collection_in_progress_ = true; + number_of_collections_++; + live_bitmap_.reset(CodeCacheBitmap::Create( + "code-cache-bitmap", + reinterpret_cast<uintptr_t>(private_region_.GetExecPages()->Begin()), + reinterpret_cast<uintptr_t>( + private_region_.GetExecPages()->Begin() + private_region_.GetCurrentCapacity() / 2))); + processed_zombie_code_.insert(zombie_code_.begin(), zombie_code_.end()); + zombie_code_.clear(); + processed_zombie_jni_code_.insert(zombie_jni_code_.begin(), zombie_jni_code_.end()); + zombie_jni_code_.clear(); // Empty osr method map, as osr compiled code will be deleted (except the ones // on thread stacks). + for (auto it = osr_code_map_.begin(); it != osr_code_map_.end(); ++it) { + processed_zombie_code_.insert(it->second); + } osr_code_map_.clear(); } + TimingLogger logger("JIT code cache timing logger", true, VLOG_IS_ON(jit)); + { + TimingLogger::ScopedTiming st("Code cache collection", &logger); - // Run a checkpoint on all threads to mark the JIT compiled code they are running. - MarkCompiledCodeOnThreadStacks(self); + { + ScopedObjectAccess soa(self); + // Run a checkpoint on all threads to mark the JIT compiled code they are running. + MarkCompiledCodeOnThreadStacks(self); + + // Remove zombie code which hasn't been marked. + RemoveUnmarkedCode(self); + } - // At this point, mutator threads are still running, and entrypoints of methods can - // change. We do know they cannot change to a code cache entry that is not marked, - // therefore we can safely remove those entries. - RemoveUnmarkedCode(self); + MutexLock mu(self, *Locks::jit_lock_); + live_bitmap_.reset(nullptr); + NotifyCollectionDone(self); + } + + Runtime::Current()->GetJit()->AddTimingLogger(logger); +} + +void JitCodeCache::NotifyCollectionDone(Thread* self) { + collection_in_progress_ = false; + gc_task_scheduled_ = false; + lock_cond_.Broadcast(self); } OatQuickMethodHeader* JitCodeCache::LookupMethodHeader(uintptr_t pc, ArtMethod* method) { @@ -1381,7 +1391,7 @@ ProfilingInfo* JitCodeCache::AddProfilingInfo(Thread* self, } if (info == nullptr) { - GarbageCollectCache(self); + IncreaseCodeCacheCapacity(self); MutexLock mu(self, *Locks::jit_lock_); info = AddProfilingInfoInternal(self, method, inline_cache_entries, branch_cache_entries); } @@ -1574,8 +1584,6 @@ bool JitCodeCache::NotifyCompilationOf(ArtMethod* method, VLOG(jit) << "Not compiling " << method->PrettyMethod() << " because it has the resolution stub"; - // Give it a new chance to be hot. - ClearMethodCounter(method, /*was_warm=*/ false); return false; } } @@ -1609,11 +1617,6 @@ bool JitCodeCache::NotifyCompilationOf(ArtMethod* method, // as well change them back as this stub shall not be collected anyway and this // can avoid a few expensive GenericJNI calls. data->UpdateEntryPoints(entrypoint); - if (collection_in_progress_) { - if (!IsInZygoteExecSpace(data->GetCode())) { - GetLiveBitmap()->AtomicTestAndSet(FromCodeToAllocation(data->GetCode())); - } - } } return new_compilation; } else { @@ -1681,15 +1684,12 @@ void JitCodeCache::InvalidateAllCompiledCode() { OatQuickMethodHeader::FromCodePointer(data.GetCode()); for (ArtMethod* method : data.GetMethods()) { if (method->GetEntryPointFromQuickCompiledCode() == method_header->GetEntryPoint()) { - ClearMethodCounter(method, /*was_warm=*/true); instr->InitializeMethodsCode(method, /*aot_code=*/ nullptr); } } } for (const auto& entry : method_code_map_) { ArtMethod* meth = entry.second; - // We were compiled, so we must be warm. - ClearMethodCounter(meth, /*was_warm=*/true); if (UNLIKELY(meth->IsObsolete())) { linker->SetEntryPointsForObsoleteMethod(meth); } else { @@ -1719,10 +1719,8 @@ void JitCodeCache::InvalidateCompiledCodeFor(ArtMethod* method, // Clear the method counter if we are running jitted code since we might want to jit this again in // the future. if (method_entrypoint == header->GetEntryPoint()) { - // The entrypoint is the one to invalidate, so we just update it to the interpreter entry point - // and clear the counter to get the method Jitted again. + // The entrypoint is the one to invalidate, so we just update it to the interpreter entry point. Runtime::Current()->GetInstrumentation()->InitializeMethodsCode(method, /*aot_code=*/ nullptr); - ClearMethodCounter(method, /*was_warm=*/ true); } else { Thread* self = Thread::Current(); ScopedDebugDisallowReadBarriers sddrb(self); diff --git a/runtime/jit/jit_code_cache.h b/runtime/jit/jit_code_cache.h index 96fc7e2706..3dd57121ca 100644 --- a/runtime/jit/jit_code_cache.h +++ b/runtime/jit/jit_code_cache.h @@ -285,11 +285,9 @@ class JitCodeCache { REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(!Locks::jit_lock_); void FreeLocked(JitMemoryRegion* region, const uint8_t* code, const uint8_t* data) - REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(Locks::jit_lock_); - // Perform a collection on the code cache. - EXPORT void GarbageCollectCache(Thread* self) + void IncreaseCodeCacheCapacity(Thread* self) REQUIRES(!Locks::jit_lock_) REQUIRES_SHARED(Locks::mutator_lock_); @@ -416,16 +414,26 @@ class JitCodeCache { } ProfilingInfo* GetProfilingInfo(ArtMethod* method, Thread* self); - void ResetHotnessCounter(ArtMethod* method, Thread* self); void MaybeUpdateInlineCache(ArtMethod* method, uint32_t dex_pc, ObjPtr<mirror::Class> cls, Thread* self) REQUIRES_SHARED(Locks::mutator_lock_); + // NO_THREAD_SAFETY_ANALYSIS because we may be called with the JIT lock held + // or not. The implementation of this method handles the two cases. + void AddZombieCode(ArtMethod* method, const void* code_ptr) NO_THREAD_SAFETY_ANALYSIS; + + EXPORT void DoCollection(Thread* self) + REQUIRES(!Locks::jit_lock_); + private: JitCodeCache(); + void AddZombieCodeInternal(ArtMethod* method, const void* code_ptr) + REQUIRES(Locks::jit_lock_) + REQUIRES_SHARED(Locks::mutator_lock_); + ProfilingInfo* AddProfilingInfoInternal(Thread* self, ArtMethod* method, const std::vector<uint32_t>& inline_cache_entries, @@ -433,20 +441,16 @@ class JitCodeCache { REQUIRES(Locks::jit_lock_) REQUIRES_SHARED(Locks::mutator_lock_); - // If a collection is in progress, wait for it to finish. Must be called with the mutator lock. - // The non-mutator lock version should be used if possible. This method will release then - // re-acquire the mutator lock. - void WaitForPotentialCollectionToCompleteRunnable(Thread* self) - REQUIRES(Locks::jit_lock_, !Roles::uninterruptible_) REQUIRES_SHARED(Locks::mutator_lock_); - // If a collection is in progress, wait for it to finish. Return // whether the thread actually waited. bool WaitForPotentialCollectionToComplete(Thread* self) - REQUIRES(Locks::jit_lock_) REQUIRES(!Locks::mutator_lock_); + REQUIRES(Locks::jit_lock_) REQUIRES_SHARED(!Locks::mutator_lock_); + + // Notify all waiting threads that a collection is done. + void NotifyCollectionDone(Thread* self) REQUIRES(Locks::jit_lock_); // Remove CHA dependents and underlying allocations for entries in `method_headers`. void FreeAllMethodHeaders(const std::unordered_set<OatQuickMethodHeader*>& method_headers) - REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(Locks::jit_lock_) REQUIRES(!Locks::cha_lock_); @@ -462,8 +466,7 @@ class JitCodeCache { // Free code and data allocations for `code_ptr`. void FreeCodeAndData(const void* code_ptr) - REQUIRES(Locks::jit_lock_) - REQUIRES_SHARED(Locks::mutator_lock_); + REQUIRES(Locks::jit_lock_); // Number of bytes allocated in the code cache. size_t CodeCacheSize() REQUIRES(!Locks::jit_lock_); @@ -477,16 +480,9 @@ class JitCodeCache { // Number of bytes allocated in the data cache. size_t DataCacheSizeLocked() REQUIRES(Locks::jit_lock_); - // Notify all waiting threads that a collection is done. - void NotifyCollectionDone(Thread* self) REQUIRES(Locks::jit_lock_); - // Return whether the code cache's capacity is at its maximum. bool IsAtMaxCapacity() const REQUIRES(Locks::jit_lock_); - void DoCollection(Thread* self) - REQUIRES(!Locks::jit_lock_) - REQUIRES_SHARED(Locks::mutator_lock_); - void RemoveUnmarkedCode(Thread* self) REQUIRES(!Locks::jit_lock_) REQUIRES_SHARED(Locks::mutator_lock_); @@ -563,18 +559,28 @@ class JitCodeCache { // ProfilingInfo objects we have allocated. SafeMap<ArtMethod*, ProfilingInfo*> profiling_infos_ GUARDED_BY(Locks::jit_lock_); + // Zombie code and JNI methods to consider for collection. + std::set<const void*> zombie_code_ GUARDED_BY(Locks::jit_lock_); + std::set<ArtMethod*> zombie_jni_code_ GUARDED_BY(Locks::jit_lock_); + + std::set<const void*> processed_zombie_code_ GUARDED_BY(Locks::jit_lock_); + std::set<ArtMethod*> processed_zombie_jni_code_ GUARDED_BY(Locks::jit_lock_); + // Methods that the zygote has compiled and can be shared across processes // forked from the zygote. ZygoteMap zygote_map_; // -------------- JIT GC related data structures ----------------------- // - // Condition to wait on during collection. + // Condition to wait on during collection and for accessing weak references in inline caches. ConditionVariable lock_cond_ GUARDED_BY(Locks::jit_lock_); // Whether there is a code cache collection in progress. bool collection_in_progress_ GUARDED_BY(Locks::jit_lock_); + // Whether a GC task is already scheduled. + bool gc_task_scheduled_ GUARDED_BY(Locks::jit_lock_); + // Bitmap for collecting code and data. std::unique_ptr<CodeCacheBitmap> live_bitmap_; diff --git a/runtime/jit/profile_saver.cc b/runtime/jit/profile_saver.cc index abf01f5498..3506faaa7f 100644 --- a/runtime/jit/profile_saver.cc +++ b/runtime/jit/profile_saver.cc @@ -335,7 +335,6 @@ class ProfileSaver::GetClassesAndMethodsHelper { REQUIRES_SHARED(Locks::mutator_lock_) : startup_(startup), profile_boot_class_path_(options.GetProfileBootClassPath()), - hot_method_sample_threshold_(CalculateHotMethodSampleThreshold(startup, options)), extra_flags_(GetExtraMethodHotnessFlags(options)), annotation_(annotation), arena_stack_(Runtime::Current()->GetArenaPool()), @@ -358,10 +357,6 @@ class ProfileSaver::GetClassesAndMethodsHelper { void CollectClasses(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_); void UpdateProfile(const std::set<std::string>& locations, ProfileCompilationInfo* profile_info); - uint32_t GetHotMethodSampleThreshold() const { - return hot_method_sample_threshold_; - } - size_t GetNumberOfHotMethods() const { return number_of_hot_methods_; } @@ -409,19 +404,6 @@ class ProfileSaver::GetClassesAndMethodsHelper { using DexFileRecordsMap = ScopedArenaHashMap<const DexFile*, DexFileRecords*>; - static uint32_t CalculateHotMethodSampleThreshold(bool startup, - const ProfileSaverOptions& options) { - Runtime* runtime = Runtime::Current(); - if (startup) { - const bool is_low_ram = runtime->GetHeap()->IsLowMemoryMode(); - return options.GetHotStartupMethodSamples(is_low_ram); - } else if (runtime->GetJit() != nullptr) { - return runtime->GetJit()->WarmMethodThreshold(); - } else { - return std::numeric_limits<uint32_t>::max(); - } - } - ALWAYS_INLINE static bool ShouldCollectClasses(bool startup) { // We only record classes for the startup case. This may change in the future. return startup; @@ -434,7 +416,6 @@ class ProfileSaver::GetClassesAndMethodsHelper { const bool startup_; const bool profile_boot_class_path_; - const uint32_t hot_method_sample_threshold_; const uint32_t extra_flags_; const ProfileCompilationInfo::ProfileSampleAnnotation annotation_; ArenaStack arena_stack_; @@ -609,7 +590,6 @@ void ProfileSaver::GetClassesAndMethodsHelper::UpdateProfile(const std::set<std: ProfileCompilationInfo* profile_info) { // Move members to local variables to allow the compiler to optimize this properly. const bool startup = startup_; - const uint32_t hot_method_sample_threshold = hot_method_sample_threshold_; const uint32_t base_flags = (startup ? Hotness::kFlagStartup : Hotness::kFlagPostStartup) | extra_flags_; @@ -619,10 +599,9 @@ void ProfileSaver::GetClassesAndMethodsHelper::UpdateProfile(const std::set<std: uint16_t initial_value = Runtime::Current()->GetJITOptions()->GetWarmupThreshold(); auto get_method_flags = [&](ArtMethod& method) { - // Mark methods as hot if they have more than hot_method_sample_threshold - // samples. This means they will get compiled by the compiler driver. - if (method.PreviouslyWarm() || - method.CounterHasReached(hot_method_sample_threshold, initial_value)) { + // Mark methods as hot if they are marked as such (warm for the runtime + // means hot for the profile). + if (method.PreviouslyWarm()) { ++number_of_hot_methods; return enum_cast<ProfileCompilationInfo::MethodHotness::Flag>(base_flags | Hotness::kFlagHot); } else if (method.CounterHasChanged(initial_value)) { @@ -750,7 +729,6 @@ void ProfileSaver::FetchAndCacheResolvedClassesAndMethods(bool startup) { profiler_pthread = profiler_pthread_; } - uint32_t hot_method_sample_threshold = 0u; size_t number_of_hot_methods = 0u; size_t number_of_sampled_methods = 0u; { @@ -765,7 +743,6 @@ void ProfileSaver::FetchAndCacheResolvedClassesAndMethods(bool startup) { ScopedObjectAccess soa(self); GetClassesAndMethodsHelper helper(startup, options_, GetProfileSampleAnnotation()); - hot_method_sample_threshold = helper.GetHotMethodSampleThreshold(); helper.CollectClasses(self); // Release the mutator lock. We shall need to re-acquire the lock for a moment to @@ -800,8 +777,7 @@ void ProfileSaver::FetchAndCacheResolvedClassesAndMethods(bool startup) { } VLOG(profiler) << "Profile saver recorded " << number_of_hot_methods << " hot methods and " << number_of_sampled_methods - << " sampled methods with threshold " << hot_method_sample_threshold - << " in " << PrettyDuration(NanoTime() - start_time); + << " sampled methods in " << PrettyDuration(NanoTime() - start_time); } bool ProfileSaver::ProcessProfilingInfo( diff --git a/runtime/jit/profile_saver_options.h b/runtime/jit/profile_saver_options.h index f6d928ff6b..ed2f00f48f 100644 --- a/runtime/jit/profile_saver_options.h +++ b/runtime/jit/profile_saver_options.h @@ -26,14 +26,10 @@ struct ProfileSaverOptions { // period is not configured. static constexpr uint32_t kMinFirstSaveMsNotSet = 0; static constexpr uint32_t kSaveResolvedClassesDelayMs = 5 * 1000; // 5 seconds - // Minimum number of JIT samples during launch to mark a method as hot in the profile. - static constexpr uint32_t kHotStartupMethodSamples = 1; - static constexpr uint32_t kHotStartupMethodSamplesLowRam = 256; static constexpr uint32_t kMinMethodsToSave = 10; static constexpr uint32_t kMinClassesToSave = 10; static constexpr uint32_t kMinNotificationBeforeWake = 10; static constexpr uint32_t kMaxNotificationBeforeWake = 50; - static constexpr uint32_t kHotStartupMethodSamplesNotSet = std::numeric_limits<uint32_t>::max(); static constexpr uint16_t kInlineCacheThreshold = 4000; ProfileSaverOptions() : @@ -41,7 +37,6 @@ struct ProfileSaverOptions { min_save_period_ms_(kMinSavePeriodMs), min_first_save_ms_(kMinFirstSaveMsNotSet), save_resolved_classes_delay_ms_(kSaveResolvedClassesDelayMs), - hot_startup_method_samples_(kHotStartupMethodSamplesNotSet), min_methods_to_save_(kMinMethodsToSave), min_classes_to_save_(kMinClassesToSave), min_notification_before_wake_(kMinNotificationBeforeWake), @@ -57,7 +52,6 @@ struct ProfileSaverOptions { uint32_t min_save_period_ms, uint32_t min_first_save_ms, uint32_t save_resolved_classes_delay_ms, - uint32_t hot_startup_method_samples, uint32_t min_methods_to_save, uint32_t min_classes_to_save, uint32_t min_notification_before_wake, @@ -71,7 +65,6 @@ struct ProfileSaverOptions { min_save_period_ms_(min_save_period_ms), min_first_save_ms_(min_first_save_ms), save_resolved_classes_delay_ms_(save_resolved_classes_delay_ms), - hot_startup_method_samples_(hot_startup_method_samples), min_methods_to_save_(min_methods_to_save), min_classes_to_save_(min_classes_to_save), min_notification_before_wake_(min_notification_before_wake), @@ -98,13 +91,6 @@ struct ProfileSaverOptions { uint32_t GetSaveResolvedClassesDelayMs() const { return save_resolved_classes_delay_ms_; } - uint32_t GetHotStartupMethodSamples(bool is_low_ram) const { - uint32_t ret = hot_startup_method_samples_; - if (ret == kHotStartupMethodSamplesNotSet) { - ret = is_low_ram ? kHotStartupMethodSamplesLowRam : kHotStartupMethodSamples; - } - return ret; - } uint32_t GetMinMethodsToSave() const { return min_methods_to_save_; } @@ -141,7 +127,6 @@ struct ProfileSaverOptions { << ", min_save_period_ms_" << pso.min_save_period_ms_ << ", min_first_save_ms_" << pso.min_first_save_ms_ << ", save_resolved_classes_delay_ms_" << pso.save_resolved_classes_delay_ms_ - << ", hot_startup_method_samples_" << pso.hot_startup_method_samples_ << ", min_methods_to_save_" << pso.min_methods_to_save_ << ", min_classes_to_save_" << pso.min_classes_to_save_ << ", min_notification_before_wake_" << pso.min_notification_before_wake_ @@ -157,9 +142,6 @@ struct ProfileSaverOptions { uint32_t min_save_period_ms_; uint32_t min_first_save_ms_; uint32_t save_resolved_classes_delay_ms_; - // Do not access hot_startup_method_samples_ directly for reading since it may be set to the - // placeholder default. - uint32_t hot_startup_method_samples_; uint32_t min_methods_to_save_; uint32_t min_classes_to_save_; uint32_t min_notification_before_wake_; diff --git a/runtime/jit/profiling_info.h b/runtime/jit/profiling_info.h index ae59d0ade1..a32b712b0c 100644 --- a/runtime/jit/profiling_info.h +++ b/runtime/jit/profiling_info.h @@ -171,14 +171,6 @@ class ProfilingInfo { return MemberOffset(OFFSETOF_MEMBER(ProfilingInfo, baseline_hotness_count_)); } - void ResetCounter() { - baseline_hotness_count_ = GetOptimizeThreshold(); - } - - bool CounterHasChanged() const { - return baseline_hotness_count_ != GetOptimizeThreshold(); - } - uint16_t GetBaselineHotnessCount() const { return baseline_hotness_count_; } diff --git a/runtime/mirror/array-inl.h b/runtime/mirror/array-inl.h index 579b1536a3..830a7e5fb6 100644 --- a/runtime/mirror/array-inl.h +++ b/runtime/mirror/array-inl.h @@ -24,6 +24,7 @@ #include "base/bit_utils.h" #include "base/casts.h" #include "class.h" +#include "class_linker.h" #include "obj_ptr-inl.h" #include "runtime.h" #include "thread-current-inl.h" @@ -99,7 +100,7 @@ inline void PrimitiveArray<T>::SetWithoutChecks(int32_t i, T value) { DCHECK_EQ(kTransactionActive, Runtime::Current()->IsActiveTransaction()); } if (kTransactionActive) { - Runtime::Current()->RecordWriteArray(this, i, GetWithoutChecks(i)); + Runtime::Current()->GetClassLinker()->RecordWriteArray(this, i, GetWithoutChecks(i)); } DCHECK(CheckIsValidIndex<kVerifyFlags>(i)) << i << " " << GetLength<kVerifyFlags>(); GetData()[i] = value; diff --git a/runtime/mirror/class-inl.h b/runtime/mirror/class-inl.h index 74776c14d4..b2c8e52351 100644 --- a/runtime/mirror/class-inl.h +++ b/runtime/mirror/class-inl.h @@ -1169,7 +1169,16 @@ inline bool Class::CanAccessMember(ObjPtr<Class> access_to, uint32_t member_flag } // Check for protected access from a sub-class, which may or may not be in the same package. if (member_flags & kAccProtected) { - if (!this->IsInterface() && this->IsSubClass(access_to)) { + // This implementation is not compliant. We should actually check whether + // the caller is a subclass of the static type of the receiver, instead of the declaring + // class of the method we are trying to access. + // + // For example, a class outside of java.lang should not ne able to access `Object.clone`, + // but this implementation allows it. + // + // To not break existing code, we decided not to fix this and accept the + // leniency. + if (access_to->IsAssignableFrom(this)) { return true; } } diff --git a/runtime/mirror/dex_cache-inl.h b/runtime/mirror/dex_cache-inl.h index d423a8edfb..8f9d624399 100644 --- a/runtime/mirror/dex_cache-inl.h +++ b/runtime/mirror/dex_cache-inl.h @@ -141,7 +141,7 @@ inline void DexCache::SetResolvedString(dex::StringIndex string_idx, ObjPtr<Stri Runtime* const runtime = Runtime::Current(); if (UNLIKELY(runtime->IsActiveTransaction())) { DCHECK(runtime->IsAotCompiler()); - runtime->RecordResolveString(this, string_idx); + runtime->GetClassLinker()->RecordResolveString(this, string_idx); } // TODO: Fine-grained marking, so that we don't need to go through all arrays in full. WriteBarrier::ForEveryFieldWrite(this); @@ -188,7 +188,7 @@ inline void DexCache::SetResolvedMethodType(dex::ProtoIndex proto_idx, MethodTyp Runtime* const runtime = Runtime::Current(); if (UNLIKELY(runtime->IsActiveTransaction())) { DCHECK(runtime->IsAotCompiler()); - runtime->RecordResolveMethodType(this, proto_idx); + runtime->GetClassLinker()->RecordResolveMethodType(this, proto_idx); } // TODO: Fine-grained marking, so that we don't need to go through all arrays in full. WriteBarrier::ForEveryFieldWrite(this); diff --git a/runtime/mirror/object-inl.h b/runtime/mirror/object-inl.h index 09f1c39058..14b9ca3af0 100644 --- a/runtime/mirror/object-inl.h +++ b/runtime/mirror/object-inl.h @@ -393,11 +393,8 @@ template<bool kTransactionActive, inline void Object::SetFieldBoolean(MemberOffset field_offset, uint8_t new_value) { VerifyTransaction<kTransactionActive, kCheckTransaction>(); if (kTransactionActive) { - Runtime::Current()->RecordWriteFieldBoolean( - this, - field_offset, - GetFieldBoolean<kVerifyFlags, kIsVolatile>(field_offset), - kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldBoolean( + this, field_offset, GetFieldBoolean<kVerifyFlags, kIsVolatile>(field_offset), kIsVolatile); } Verify<kVerifyFlags>(); SetFieldPrimitive<uint8_t, kIsVolatile>(field_offset, new_value); @@ -410,10 +407,8 @@ template<bool kTransactionActive, inline void Object::SetFieldByte(MemberOffset field_offset, int8_t new_value) { VerifyTransaction<kTransactionActive, kCheckTransaction>(); if (kTransactionActive) { - Runtime::Current()->RecordWriteFieldByte(this, - field_offset, - GetFieldByte<kVerifyFlags, kIsVolatile>(field_offset), - kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldByte( + this, field_offset, GetFieldByte<kVerifyFlags, kIsVolatile>(field_offset), kIsVolatile); } Verify<kVerifyFlags>(); SetFieldPrimitive<int8_t, kIsVolatile>(field_offset, new_value); @@ -460,10 +455,8 @@ template<bool kTransactionActive, inline void Object::SetFieldChar(MemberOffset field_offset, uint16_t new_value) { VerifyTransaction<kTransactionActive, kCheckTransaction>(); if (kTransactionActive) { - Runtime::Current()->RecordWriteFieldChar(this, - field_offset, - GetFieldChar<kVerifyFlags, kIsVolatile>(field_offset), - kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldChar( + this, field_offset, GetFieldChar<kVerifyFlags, kIsVolatile>(field_offset), kIsVolatile); } Verify<kVerifyFlags>(); SetFieldPrimitive<uint16_t, kIsVolatile>(field_offset, new_value); @@ -476,10 +469,8 @@ template<bool kTransactionActive, inline void Object::SetFieldShort(MemberOffset field_offset, int16_t new_value) { VerifyTransaction<kTransactionActive, kCheckTransaction>(); if (kTransactionActive) { - Runtime::Current()->RecordWriteFieldChar(this, - field_offset, - GetFieldShort<kVerifyFlags, kIsVolatile>(field_offset), - kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldChar( + this, field_offset, GetFieldShort<kVerifyFlags, kIsVolatile>(field_offset), kIsVolatile); } Verify<kVerifyFlags>(); SetFieldPrimitive<int16_t, kIsVolatile>(field_offset, new_value); @@ -504,10 +495,8 @@ template<bool kTransactionActive, inline void Object::SetField32(MemberOffset field_offset, int32_t new_value) { VerifyTransaction<kTransactionActive, kCheckTransaction>(); if (kTransactionActive) { - Runtime::Current()->RecordWriteField32(this, - field_offset, - GetField32<kVerifyFlags, kIsVolatile>(field_offset), - kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteField32( + this, field_offset, GetField32<kVerifyFlags, kIsVolatile>(field_offset), kIsVolatile); } Verify<kVerifyFlags>(); SetFieldPrimitive<int32_t, kIsVolatile>(field_offset, new_value); @@ -534,10 +523,8 @@ template<bool kTransactionActive, inline void Object::SetField64(MemberOffset field_offset, int64_t new_value) { VerifyTransaction<kTransactionActive, kCheckTransaction>(); if (kTransactionActive) { - Runtime::Current()->RecordWriteField64(this, - field_offset, - GetField64<kVerifyFlags, kIsVolatile>(field_offset), - kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteField64( + this, field_offset, GetField64<kVerifyFlags, kIsVolatile>(field_offset), kIsVolatile); } Verify<kVerifyFlags>(); SetFieldPrimitive<int64_t, kIsVolatile>(field_offset, new_value); @@ -575,7 +562,8 @@ inline bool Object::CasFieldWeakSequentiallyConsistent64(MemberOffset field_offs Atomic<int64_t>* atomic_addr = reinterpret_cast<Atomic<int64_t>*>(raw_addr); bool success = atomic_addr->CompareAndSetWeakSequentiallyConsistent(old_value, new_value); if (kTransactionActive && success) { - Runtime::Current()->RecordWriteField64(this, field_offset, old_value, /*is_volatile=*/ true); + Runtime::Current()->GetClassLinker()->RecordWriteField64( + this, field_offset, old_value, /*is_volatile=*/ true); } return success; } @@ -590,7 +578,8 @@ inline bool Object::CasFieldStrongSequentiallyConsistent64(MemberOffset field_of Atomic<int64_t>* atomic_addr = reinterpret_cast<Atomic<int64_t>*>(raw_addr); bool success = atomic_addr->CompareAndSetStrongSequentiallyConsistent(old_value, new_value); if (kTransactionActive && success) { - Runtime::Current()->RecordWriteField64(this, field_offset, old_value, /*is_volatile=*/ true); + Runtime::Current()->GetClassLinker()->RecordWriteField64( + this, field_offset, old_value, /*is_volatile=*/ true); } return success; } @@ -630,7 +619,8 @@ inline void Object::SetFieldObjectWithoutWriteBarrier(MemberOffset field_offset, if (kTransactionActive) { ObjPtr<Object> old_value = GetFieldObject<Object, kVerifyFlags, kWithReadBarrier, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteFieldReference(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldReference( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); VerifyWrite<kVerifyFlags>(new_value); @@ -689,7 +679,7 @@ inline bool Object::CasFieldObjectWithoutWriteBarrier(MemberOffset field_offset, Atomic<uint32_t>* atomic_addr = reinterpret_cast<Atomic<uint32_t>*>(raw_addr); bool success = atomic_addr->CompareAndSet(old_ref, new_ref, mode, memory_order); if (kTransactionActive && success) { - Runtime::Current()->RecordWriteFieldReference( + Runtime::Current()->GetClassLinker()->RecordWriteFieldReference( this, field_offset, old_value, /*is_volatile=*/ true); } return success; @@ -731,7 +721,8 @@ inline ObjPtr<Object> Object::CompareAndExchangeFieldObject(MemberOffset field_o } if (success) { if (kTransactionActive) { - Runtime::Current()->RecordWriteFieldReference(this, field_offset, witness_value, true); + Runtime::Current()->GetClassLinker()->RecordWriteFieldReference( + this, field_offset, witness_value, /*is_volatile=*/ true); } WriteBarrier::ForFieldWrite(this, field_offset, new_value); } @@ -755,7 +746,8 @@ inline ObjPtr<Object> Object::ExchangeFieldObject(MemberOffset field_offset, ReadBarrier::AssertToSpaceInvariant(old_value.Ptr()); } if (kTransactionActive) { - Runtime::Current()->RecordWriteFieldReference(this, field_offset, old_value, true); + Runtime::Current()->GetClassLinker()->RecordWriteFieldReference( + this, field_offset, old_value, /*is_volatile=*/ true); } WriteBarrier::ForFieldWrite(this, field_offset, new_value); VerifyRead<kVerifyFlags>(old_value); @@ -777,7 +769,8 @@ inline void Object::UpdateFieldBooleanViaAccessor(MemberOffset field_offset, if (kTransactionActive) { static const bool kIsVolatile = true; uint8_t old_value = GetFieldBoolean<kVerifyFlags, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteFieldBoolean(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldBoolean( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); uint8_t* raw_addr = reinterpret_cast<uint8_t*>(this) + field_offset.Int32Value(); @@ -792,7 +785,8 @@ inline void Object::UpdateFieldByteViaAccessor(MemberOffset field_offset, if (kTransactionActive) { static const bool kIsVolatile = true; int8_t old_value = GetFieldByte<kVerifyFlags, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteFieldByte(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldByte( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); uint8_t* raw_addr = reinterpret_cast<uint8_t*>(this) + field_offset.Int32Value(); @@ -807,7 +801,8 @@ inline void Object::UpdateFieldCharViaAccessor(MemberOffset field_offset, if (kTransactionActive) { static const bool kIsVolatile = true; uint16_t old_value = GetFieldChar<kVerifyFlags, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteFieldChar(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldChar( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); uint8_t* raw_addr = reinterpret_cast<uint8_t*>(this) + field_offset.Int32Value(); @@ -822,7 +817,8 @@ inline void Object::UpdateFieldShortViaAccessor(MemberOffset field_offset, if (kTransactionActive) { static const bool kIsVolatile = true; int16_t old_value = GetFieldShort<kVerifyFlags, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteFieldShort(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteFieldShort( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); uint8_t* raw_addr = reinterpret_cast<uint8_t*>(this) + field_offset.Int32Value(); @@ -837,7 +833,8 @@ inline void Object::UpdateField32ViaAccessor(MemberOffset field_offset, if (kTransactionActive) { static const bool kIsVolatile = true; int32_t old_value = GetField32<kVerifyFlags, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteField32(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteField32( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); uint8_t* raw_addr = reinterpret_cast<uint8_t*>(this) + field_offset.Int32Value(); @@ -852,7 +849,8 @@ inline void Object::UpdateField64ViaAccessor(MemberOffset field_offset, if (kTransactionActive) { static const bool kIsVolatile = true; int64_t old_value = GetField64<kVerifyFlags, kIsVolatile>(field_offset); - Runtime::Current()->RecordWriteField64(this, field_offset, old_value, kIsVolatile); + Runtime::Current()->GetClassLinker()->RecordWriteField64( + this, field_offset, old_value, kIsVolatile); } Verify<kVerifyFlags>(); uint8_t* raw_addr = reinterpret_cast<uint8_t*>(this) + field_offset.Int32Value(); diff --git a/runtime/mirror/object-readbarrier-inl.h b/runtime/mirror/object-readbarrier-inl.h index 6c201f2277..39bc43b63a 100644 --- a/runtime/mirror/object-readbarrier-inl.h +++ b/runtime/mirror/object-readbarrier-inl.h @@ -20,6 +20,7 @@ #include "object.h" #include "base/atomic.h" +#include "class_linker.h" #include "heap_poisoning.h" #include "lock_word-inl.h" #include "object_reference-inl.h" @@ -54,7 +55,8 @@ inline bool Object::CasField32(MemberOffset field_offset, bool success = atomic_addr->CompareAndSet(old_value, new_value, mode, memory_order); if (kTransactionActive && success) { - Runtime::Current()->RecordWriteField32(this, field_offset, old_value, /*is_volatile=*/ true); + Runtime::Current()->GetClassLinker()->RecordWriteField32( + this, field_offset, old_value, /*is_volatile=*/ true); } return success; } diff --git a/runtime/native/dalvik_system_VMDebug.cc b/runtime/native/dalvik_system_VMDebug.cc index 137b04fede..18120fe826 100644 --- a/runtime/native/dalvik_system_VMDebug.cc +++ b/runtime/native/dalvik_system_VMDebug.cc @@ -55,13 +55,16 @@ namespace art HIDDEN { static jobjectArray VMDebug_getVmFeatureList(JNIEnv* env, jclass) { ScopedObjectAccess soa(Thread::ForEnv(env)); - return soa.AddLocalReference<jobjectArray>(CreateStringArray(soa.Self(), { - "method-trace-profiling", - "method-trace-profiling-streaming", - "method-sample-profiling", - "hprof-heap-dump", - "hprof-heap-dump-streaming", - })); + return soa.AddLocalReference<jobjectArray>( + CreateStringArray(soa.Self(), + { + "method-trace-profiling", + "method-trace-profiling-streaming", + "method-sample-profiling", + "hprof-heap-dump", + "hprof-heap-dump-streaming", + "app_info", + })); } static void VMDebug_startAllocCounting(JNIEnv*, jclass) { @@ -514,6 +517,49 @@ static void VMDebug_setAllocTrackerStackDepth(JNIEnv* env, jclass, jint stack_de } } +static void VMDebug_setCurrentProcessName(JNIEnv* env, jclass, jstring process_name) { + ScopedFastNativeObjectAccess soa(env); + + // Android application ID naming convention states: + // "The name can contain uppercase or lowercase letters, numbers, and underscores ('_')" + // This is fine to convert to std::string + const char* c_process_name = env->GetStringUTFChars(process_name, NULL); + Runtime::Current()->GetRuntimeCallbacks()->SetCurrentProcessName(std::string(c_process_name)); + env->ReleaseStringUTFChars(process_name, c_process_name); +} + +static void VMDebug_addApplication(JNIEnv* env, jclass, jstring package_name) { + ScopedFastNativeObjectAccess soa(env); + + // Android application ID naming convention states: + // "The name can contain uppercase or lowercase letters, numbers, and underscores ('_')" + // This is fine to convert to std::string + const char* c_package_name = env->GetStringUTFChars(package_name, NULL); + Runtime::Current()->GetRuntimeCallbacks()->AddApplication(std::string(c_package_name)); + env->ReleaseStringUTFChars(package_name, c_package_name); +} + +static void VMDebug_removeApplication(JNIEnv* env, jclass, jstring package_name) { + ScopedFastNativeObjectAccess soa(env); + + // Android application ID naming convention states: + // "The name can contain uppercase or lowercase letters, numbers, and underscores ('_')" + // This is fine to convert to std::string + const char* c_package_name = env->GetStringUTFChars(package_name, NULL); + Runtime::Current()->GetRuntimeCallbacks()->RemoveApplication(std::string(c_package_name)); + env->ReleaseStringUTFChars(package_name, c_package_name); +} + +static void VMDebug_setWaitingForDebugger(JNIEnv* env, jclass, jboolean waiting) { + ScopedFastNativeObjectAccess soa(env); + Runtime::Current()->GetRuntimeCallbacks()->SetWaitingForDebugger(waiting); +} + +static void VMDebug_setUserId(JNIEnv* env, jclass, jint user_id) { + ScopedFastNativeObjectAccess soa(env); + Runtime::Current()->GetRuntimeCallbacks()->SetUserId(user_id); +} + static JNINativeMethod gMethods[] = { NATIVE_METHOD(VMDebug, countInstancesOfClass, "(Ljava/lang/Class;Z)J"), NATIVE_METHOD(VMDebug, countInstancesOfClasses, "([Ljava/lang/Class;Z)[J"), @@ -542,6 +588,11 @@ static JNINativeMethod gMethods[] = { NATIVE_METHOD(VMDebug, nativeAttachAgent, "(Ljava/lang/String;Ljava/lang/ClassLoader;)V"), NATIVE_METHOD(VMDebug, allowHiddenApiReflectionFrom, "(Ljava/lang/Class;)V"), NATIVE_METHOD(VMDebug, setAllocTrackerStackDepth, "(I)V"), + NATIVE_METHOD(VMDebug, setCurrentProcessName, "(Ljava/lang/String;)V"), + NATIVE_METHOD(VMDebug, setWaitingForDebugger, "(Z)V"), + NATIVE_METHOD(VMDebug, addApplication, "(Ljava/lang/String;)V"), + NATIVE_METHOD(VMDebug, removeApplication, "(Ljava/lang/String;)V"), + NATIVE_METHOD(VMDebug, setUserId, "(I)V"), }; void register_dalvik_system_VMDebug(JNIEnv* env) { diff --git a/runtime/native_gc_triggering.md b/runtime/native_gc_triggering.md new file mode 100644 index 0000000000..3e8fb08f01 --- /dev/null +++ b/runtime/native_gc_triggering.md @@ -0,0 +1,87 @@ +Triggering the GC to reclaim non-Java memory +-------------------------------------------- + +Android applications and libraries commonly allocate "native" (i.e. C++) objects that are +effectively owned by a Java object, and reclaimed when the garbage collector determines that the +owning Java object is no longer reachable. Various mechanisms are used to accomplish this. These +include traditional Java finalizers, more modern Java `Cleaner`s, Android's `SystemCleaner` and, +for platform code, `NativeAllocationRegistry`. Internally, these all rely on the garbage +collector's processing of `java.lang.ref.Reference`s. + +Historically, we have encountered issues when large volumes of such "native" objects are owned by +Java objects of significantly smaller size. The Java garbage collector normally decides when to +collect based on occupancy of the Java heap. It would not collect if there are few bytes of newly +allocated Java objects in the Java heap, even if they "own" large amounts of C++ memory, which +cannot be reclaimed until the Java objects are reclaimed. + +This led to the development of the `VMRuntime.registerNativeAllocation` API, and eventually the +`NativeAllocationRegistry` API. Both of these allow the programmer to inform the ART GC that a +certain amount of C++ memory had been allocated, and could only be reclaimed as the result of a +Java GC. + +This had the advantage that the GC would theoretically know exactly how much native memory was +owned by Java objects. However, its major problem was that registering the exact size of such +native objects frequently turned out to be impractical. Often a Java object does not just own a +single native object, but instead owns a complex native data structure composed of many objects in +the C++ heap. Their sizes commonly depended on prior computations by C++ code. Some of these might +be shared between Java objects, and could only be reclaimed when all of those Java objects became +unreachable. Often at least some of the native objects were allocated by third-party libraries +that did not make the sizes of its internal objects available to clients. + +In extreme cases, underestimation of native object sizes could cause native memory use to be so +excessive that the device would become unstable. At one time, a particularly nasty arithmetic +expression would cause the Google Calculator app to allocate sufficiently large native arrays of +digits backing Java `BigInteger`s to force a restart of system processes. (This has since been +addressed in other ways as well.) + +Thus we switched to a scheme in which sizes of native objects are commonly no longer directly +provided by clients. The GC instead occasionally calls `mallinfo()` to determine how much native +memory has been allocated, and assumes that any of this may need the collector's help to reclaim. +C++ memory allocated by means other than `malloc`/`new` and owned by Java objects should still be +explicitly registered. This loses information about Java ownership, but results in much more +accurate information about the total size of native objects, something that was previously very +difficult to approximate, even to within an order of magnitude. + +The triggering heuristic +------------------------ + +We normally check for the need to trigger a GC due to native memory pressure only as a result of +calls to the `NativeAllocationRegistry` or `VMRuntime.registerNativeAllocation` APIs. Thus an +application not using these APIs, e.g. because it is running almost entirely native code, may +never do so. This is generally a feature, rather than a bug. + +The actual computation for triggering a native-allocation-GC is performed by +`Heap::NativeMemoryOverTarget()`. This computes and compares two quantities: + +1. An adjusted heap size for GC triggering. This consists of the Java heap size at which we would + normally trigger a GC plus an allowance for native heap size. This allowance currently consists + of one half (background processes) or three halves (foreground processes) of + `NativeAllocationGcWatermark()`. The latter is HeapMaxFree (typically 32MB) plus 1/8 of the + currently targeted heap size. For a foreground process, this allowance would typically be in + the 50-100 MB range for something other than a low-end device. + +2. An adjusted count of current bytes allocated. This is basically the number of bytes currently + allocated in the Java heap, plus half the net number of native bytes allocated since the last + GC. The latter is computed as the change since the last GC in the total-bytes-allocated + obtained from `mallinfo()` plus `native_bytes_registered_`. The `native_bytes_registered_` + field tracks the bytes explicitly registered, and not yet unregistered via + `registerNativeAllocation` APIs. It excludes bytes registered as malloc-allocated via + `NativeAllocationRegistry`. (The computation also considers the total amount of native memory + currently allocated by this metric, as opposed to the change since the last GC. But that is + currently de-weighted to the point of insignificance.) + +A background GC is triggered if the second quantity exceeds the first. A stop-the-world-GC is +triggered if the second quantity is at least 4 times larger than the first, and the native heap +currently occupies a large fraction of device memory, suggesting that the GC is falling behind and +endangering device usability. + +The fact that we consider both Java and native memory use at once means that we are more likely to +trigger a native GC when we are closer to the normal Java GC threshold. + +The actual use of `mallinfo()` to compute native heap occupancy reflects experiments with +different `libc` implementations. These have different performance characteristics, and sometimes +disagree on the interpretation of `struct mallinfo` fields. We believe our current implementation +is solid with scudo and jemalloc on Android, and minimally usable for testing elsewhere. + +(Some of this assumes typical current (May 2024) configuration constants, and may need to be +updated.) diff --git a/runtime/oat/aot_class_linker.cc b/runtime/oat/aot_class_linker.cc index e1ca63d44e..0a85fa094b 100644 --- a/runtime/oat/aot_class_linker.cc +++ b/runtime/oat/aot_class_linker.cc @@ -22,23 +22,24 @@ #include "dex/class_reference.h" #include "gc/heap.h" #include "handle_scope-inl.h" -#include "interpreter/active_transaction_checker.h" #include "mirror/class-inl.h" #include "runtime.h" +#include "scoped_thread_state_change-inl.h" +#include "transaction.h" #include "verifier/verifier_enums.h" namespace art HIDDEN { AotClassLinker::AotClassLinker(InternTable* intern_table) - : ClassLinker(intern_table, /*fast_class_not_found_exceptions=*/ false) {} + : ClassLinker(intern_table, /*fast_class_not_found_exceptions=*/ false), + preinitialization_transactions_() {} AotClassLinker::~AotClassLinker() {} bool AotClassLinker::CanAllocClass() { // AllocClass doesn't work under transaction, so we abort. - if (Runtime::Current()->IsActiveTransaction()) { - Runtime::Current()->AbortTransactionAndThrowAbortError( - Thread::Current(), "Can't resolve type within transaction."); + if (IsActiveTransaction()) { + AbortTransactionF(Thread::Current(), "Can't resolve type within transaction."); return false; } return ClassLinker::CanAllocClass(); @@ -49,8 +50,7 @@ bool AotClassLinker::InitializeClass(Thread* self, Handle<mirror::Class> klass, bool can_init_statics, bool can_init_parents) { - Runtime* const runtime = Runtime::Current(); - bool strict_mode = runtime->IsActiveStrictTransactionMode(); + bool strict_mode = IsActiveStrictTransactionMode(); DCHECK(klass != nullptr); if (klass->IsInitialized() || klass->IsInitializing()) { @@ -61,10 +61,12 @@ bool AotClassLinker::InitializeClass(Thread* self, // in a dex file belonging to the boot image we're compiling against. // However, we must allow the initialization of TransactionAbortError, // VerifyError, etc. outside of a transaction. - if (!strict_mode && runtime->GetHeap()->ObjectIsInBootImageSpace(klass->GetDexCache())) { - if (runtime->IsActiveTransaction()) { - runtime->AbortTransactionAndThrowAbortError(self, "Can't initialize " + klass->PrettyTypeOf() - + " because it is defined in a boot image dex file."); + if (!strict_mode && + Runtime::Current()->GetHeap()->ObjectIsInBootImageSpace(klass->GetDexCache())) { + if (IsActiveTransaction()) { + AbortTransactionF(self, + "Can't initialize %s because it is defined in a boot image dex file.", + klass->PrettyTypeOf().c_str()); return false; } CHECK(klass->IsThrowableClass()) << klass->PrettyDescriptor(); @@ -72,8 +74,9 @@ bool AotClassLinker::InitializeClass(Thread* self, // When in strict_mode, don't initialize a class if it belongs to boot but not initialized. if (strict_mode && klass->IsBootStrapClassLoaded()) { - runtime->AbortTransactionAndThrowAbortError(self, "Can't resolve " - + klass->PrettyTypeOf() + " because it is an uninitialized boot class."); + AbortTransactionF(self, + "Can't resolve %s because it is an uninitialized boot class.", + klass->PrettyTypeOf().c_str()); return false; } @@ -81,21 +84,22 @@ bool AotClassLinker::InitializeClass(Thread* self, // the transaction and rolled back after klass's change is commited. if (strict_mode && !klass->IsInterface() && klass->HasSuperClass()) { if (klass->GetSuperClass()->GetStatus() == ClassStatus::kInitializing) { - runtime->AbortTransactionAndThrowAbortError(self, "Can't resolve " - + klass->PrettyTypeOf() + " because it's superclass is not initialized."); + AbortTransactionF(self, + "Can't resolve %s because it's superclass is not initialized.", + klass->PrettyTypeOf().c_str()); return false; } } if (strict_mode) { - runtime->EnterTransactionMode(/*strict=*/ true, klass.Get()); + EnterTransactionMode(/*strict=*/ true, klass.Get()); } bool success = ClassLinker::InitializeClass(self, klass, can_init_statics, can_init_parents); if (strict_mode) { if (success) { // Exit Transaction if success. - runtime->ExitTransactionMode(); + ExitTransactionMode(); } else { // If not successfully initialized, don't rollback immediately, leave the cleanup to compiler // driver which needs abort message and exception. @@ -251,21 +255,260 @@ void AotClassLinker::SetEnablePublicSdkChecks(bool enabled) { } } -bool AotClassLinker::TransactionWriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) const { - DCHECK(Runtime::Current()->IsActiveTransaction()); - return interpreter::ActiveTransactionChecker::WriteConstraint(self, obj); +// Transaction support. + +bool AotClassLinker::IsActiveTransaction() const { + bool result = Runtime::Current()->IsActiveTransaction(); + DCHECK_EQ(result, !preinitialization_transactions_.empty() && !GetTransaction()->IsRollingBack()); + return result; +} + +void AotClassLinker::EnterTransactionMode(bool strict, mirror::Class* root) { + Runtime* runtime = Runtime::Current(); + ArenaPool* arena_pool = nullptr; + ArenaStack* arena_stack = nullptr; + if (preinitialization_transactions_.empty()) { // Top-level transaction? + // Make initialized classes visibly initialized now. If that happened during the transaction + // and then the transaction was aborted, we would roll back the status update but not the + // ClassLinker's bookkeeping structures, so these classes would never be visibly initialized. + { + Thread* self = Thread::Current(); + StackHandleScope<1> hs(self); + HandleWrapper<mirror::Class> h(hs.NewHandleWrapper(&root)); + ScopedThreadSuspension sts(self, ThreadState::kNative); + MakeInitializedClassesVisiblyInitialized(Thread::Current(), /*wait=*/ true); + } + // Pass the runtime `ArenaPool` to the transaction. + arena_pool = runtime->GetArenaPool(); + } else { + // Pass the `ArenaStack` from previous transaction to the new one. + arena_stack = preinitialization_transactions_.front().GetArenaStack(); + } + preinitialization_transactions_.emplace_front(strict, root, arena_stack, arena_pool); + runtime->SetActiveTransaction(); +} + +void AotClassLinker::ExitTransactionMode() { + DCHECK(IsActiveTransaction()); + preinitialization_transactions_.pop_front(); + if (preinitialization_transactions_.empty()) { + Runtime::Current()->ClearActiveTransaction(); + } else { + DCHECK(IsActiveTransaction()); // Trigger the DCHECK() in `IsActiveTransaction()`. + } +} + +void AotClassLinker::RollbackAllTransactions() { + // If transaction is aborted, all transactions will be kept in the list. + // Rollback and exit all of them. + while (IsActiveTransaction()) { + RollbackAndExitTransactionMode(); + } +} + +void AotClassLinker::RollbackAndExitTransactionMode() { + DCHECK(IsActiveTransaction()); + Runtime::Current()->ClearActiveTransaction(); + preinitialization_transactions_.front().Rollback(); + preinitialization_transactions_.pop_front(); + if (!preinitialization_transactions_.empty()) { + Runtime::Current()->SetActiveTransaction(); + } +} + +const Transaction* AotClassLinker::GetTransaction() const { + DCHECK(!preinitialization_transactions_.empty()); + return &preinitialization_transactions_.front(); +} + +Transaction* AotClassLinker::GetTransaction() { + DCHECK(!preinitialization_transactions_.empty()); + return &preinitialization_transactions_.front(); +} + +bool AotClassLinker::IsActiveStrictTransactionMode() const { + return IsActiveTransaction() && GetTransaction()->IsStrict(); +} + +bool AotClassLinker::TransactionWriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) { + DCHECK(IsActiveTransaction()); + if (GetTransaction()->WriteConstraint(obj)) { + Runtime* runtime = Runtime::Current(); + DCHECK(runtime->GetHeap()->ObjectIsInBootImageSpace(obj) || obj->IsClass()); + const char* extra = runtime->GetHeap()->ObjectIsInBootImageSpace(obj) ? "boot image " : ""; + AbortTransactionF( + self, "Can't set fields of %s%s", extra, obj->PrettyTypeOf().c_str()); + return true; + } + return false; +} + +bool AotClassLinker::TransactionWriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) { + DCHECK(IsActiveTransaction()); + if (GetTransaction()->WriteValueConstraint(value)) { + DCHECK(value != nullptr); + const char* description = value->IsClass() ? "class" : "instance of"; + ObjPtr<mirror::Class> klass = value->IsClass() ? value->AsClass() : value->GetClass(); + AbortTransactionF( + self, "Can't store reference to %s %s", description, klass->PrettyDescriptor().c_str()); + return true; + } + return false; +} + +bool AotClassLinker::TransactionReadConstraint(Thread* self, ObjPtr<mirror::Object> obj) { + DCHECK(obj->IsClass()); + if (GetTransaction()->ReadConstraint(obj)) { + AbortTransactionF(self, + "Can't read static fields of %s since it does not belong to clinit's class.", + obj->PrettyTypeOf().c_str()); + return true; + } + return false; +} + +bool AotClassLinker::TransactionAllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) { + DCHECK(IsActiveTransaction()); + if (klass->IsFinalizable()) { + AbortTransactionF(self, + "Allocating finalizable object in transaction: %s", + klass->PrettyDescriptor().c_str()); + return true; + } + return false; +} + +void AotClassLinker::RecordWriteFieldBoolean(mirror::Object* obj, + MemberOffset field_offset, + uint8_t value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteFieldBoolean(obj, field_offset, value, is_volatile); +} + +void AotClassLinker::RecordWriteFieldByte(mirror::Object* obj, + MemberOffset field_offset, + int8_t value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteFieldByte(obj, field_offset, value, is_volatile); +} + +void AotClassLinker::RecordWriteFieldChar(mirror::Object* obj, + MemberOffset field_offset, + uint16_t value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteFieldChar(obj, field_offset, value, is_volatile); +} + +void AotClassLinker::RecordWriteFieldShort(mirror::Object* obj, + MemberOffset field_offset, + int16_t value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteFieldShort(obj, field_offset, value, is_volatile); +} + +void AotClassLinker::RecordWriteField32(mirror::Object* obj, + MemberOffset field_offset, + uint32_t value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteField32(obj, field_offset, value, is_volatile); +} + +void AotClassLinker::RecordWriteField64(mirror::Object* obj, + MemberOffset field_offset, + uint64_t value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteField64(obj, field_offset, value, is_volatile); +} + +void AotClassLinker::RecordWriteFieldReference(mirror::Object* obj, + MemberOffset field_offset, + ObjPtr<mirror::Object> value, + bool is_volatile) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteFieldReference(obj, field_offset, value.Ptr(), is_volatile); +} + +void AotClassLinker::RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWriteArray(array, index, value); +} + +void AotClassLinker::RecordStrongStringInsertion(ObjPtr<mirror::String> s) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordStrongStringInsertion(s); +} + +void AotClassLinker::RecordWeakStringInsertion(ObjPtr<mirror::String> s) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWeakStringInsertion(s); +} + +void AotClassLinker::RecordStrongStringRemoval(ObjPtr<mirror::String> s) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordStrongStringRemoval(s); +} + +void AotClassLinker::RecordWeakStringRemoval(ObjPtr<mirror::String> s) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordWeakStringRemoval(s); +} + +void AotClassLinker::RecordResolveString(ObjPtr<mirror::DexCache> dex_cache, + dex::StringIndex string_idx) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordResolveString(dex_cache, string_idx); } -bool AotClassLinker::TransactionWriteValueConstraint( - Thread* self, ObjPtr<mirror::Object> value) const { - DCHECK(Runtime::Current()->IsActiveTransaction()); - return interpreter::ActiveTransactionChecker::WriteValueConstraint(self, value); +void AotClassLinker::RecordResolveMethodType(ObjPtr<mirror::DexCache> dex_cache, + dex::ProtoIndex proto_idx) { + DCHECK(IsActiveTransaction()); + GetTransaction()->RecordResolveMethodType(dex_cache, proto_idx); } -bool AotClassLinker::TransactionAllocationConstraint(Thread* self, - ObjPtr<mirror::Class> klass) const { - DCHECK(Runtime::Current()->IsActiveTransaction()); - return interpreter::ActiveTransactionChecker::AllocationConstraint(self, klass); +void AotClassLinker::ThrowTransactionAbortError(Thread* self) { + DCHECK(IsActiveTransaction()); + // Passing nullptr means we rethrow an exception with the earlier transaction abort message. + GetTransaction()->ThrowAbortError(self, nullptr); +} + +void AotClassLinker::AbortTransactionF(Thread* self, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + AbortTransactionV(self, fmt, args); + va_end(args); +} + +void AotClassLinker::AbortTransactionV(Thread* self, const char* fmt, va_list args) { + CHECK(IsActiveTransaction()); + // Constructs abort message. + std::string abort_message; + android::base::StringAppendV(&abort_message, fmt, args); + // Throws an exception so we can abort the transaction and rollback every change. + // + // Throwing an exception may cause its class initialization. If we mark the transaction + // aborted before that, we may warn with a false alarm. Throwing the exception before + // marking the transaction aborted avoids that. + // But now the transaction can be nested, and abort the transaction will relax the constraints + // for constructing stack trace. + GetTransaction()->Abort(abort_message); + GetTransaction()->ThrowAbortError(self, &abort_message); +} + +bool AotClassLinker::IsTransactionAborted() const { + DCHECK(IsActiveTransaction()); + return GetTransaction()->IsAborted(); +} + +void AotClassLinker::VisitTransactionRoots(RootVisitor* visitor) { + for (Transaction& transaction : preinitialization_transactions_) { + transaction.VisitRoots(visitor); + } } } // namespace art diff --git a/runtime/oat/aot_class_linker.h b/runtime/oat/aot_class_linker.h index b046f1e66a..0ee2fe1b42 100644 --- a/runtime/oat/aot_class_linker.h +++ b/runtime/oat/aot_class_linker.h @@ -17,12 +17,16 @@ #ifndef ART_RUNTIME_OAT_AOT_CLASS_LINKER_H_ #define ART_RUNTIME_OAT_AOT_CLASS_LINKER_H_ +#include <forward_list> + #include "base/macros.h" #include "sdk_checker.h" #include "class_linker.h" namespace art HIDDEN { +class Transaction; + namespace gc { class Heap; } // namespace gc @@ -43,19 +47,98 @@ class AotClassLinker : public ClassLinker { EXPORT void SetSdkChecker(std::unique_ptr<SdkChecker>&& sdk_checker_); const SdkChecker* GetSdkChecker() const; + // Verifies if the method is accessible according to the SdkChecker (if installed). bool DenyAccessBasedOnPublicSdk(ArtMethod* art_method) const override REQUIRES_SHARED(Locks::mutator_lock_); + // Verifies if the field is accessible according to the SdkChecker (if installed). bool DenyAccessBasedOnPublicSdk(ArtField* art_field) const override REQUIRES_SHARED(Locks::mutator_lock_); + // Verifies if the descriptor is accessible according to the SdkChecker (if installed). bool DenyAccessBasedOnPublicSdk(std::string_view type_descriptor) const override; + // Enable or disable public sdk checks. void SetEnablePublicSdkChecks(bool enabled) override; + // Transaction support. + EXPORT bool IsActiveTransaction() const; + // EnterTransactionMode may suspend. + EXPORT void EnterTransactionMode(bool strict, mirror::Class* root) + REQUIRES_SHARED(Locks::mutator_lock_); + EXPORT void ExitTransactionMode(); + EXPORT void RollbackAllTransactions() REQUIRES_SHARED(Locks::mutator_lock_); + // Transaction rollback and exit transaction are always done together, it's convenience to + // do them in one function. + void RollbackAndExitTransactionMode() REQUIRES_SHARED(Locks::mutator_lock_); + const Transaction* GetTransaction() const; + Transaction* GetTransaction(); + bool IsActiveStrictTransactionMode() const; + // Transaction constraint checks for AOT compilation. - bool TransactionWriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) const override + bool TransactionWriteConstraint(Thread* self, ObjPtr<mirror::Object> obj) override + REQUIRES_SHARED(Locks::mutator_lock_); + bool TransactionWriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) override + REQUIRES_SHARED(Locks::mutator_lock_); + // Note: The read constraint check is non-virtual because it's not needed by `UnstartedRuntime`. + bool TransactionReadConstraint(Thread* self, ObjPtr<mirror::Object> obj) + REQUIRES_SHARED(Locks::mutator_lock_); + bool TransactionAllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) override + REQUIRES_SHARED(Locks::mutator_lock_); + + // Transaction bookkeeping for AOT compilation. + void RecordWriteFieldBoolean(mirror::Object* obj, + MemberOffset field_offset, + uint8_t value, + bool is_volatile) override; + void RecordWriteFieldByte(mirror::Object* obj, + MemberOffset field_offset, + int8_t value, + bool is_volatile) override; + void RecordWriteFieldChar(mirror::Object* obj, + MemberOffset field_offset, + uint16_t value, + bool is_volatile) override; + void RecordWriteFieldShort(mirror::Object* obj, + MemberOffset field_offset, + int16_t value, + bool is_volatile) override; + void RecordWriteField32(mirror::Object* obj, + MemberOffset field_offset, + uint32_t value, + bool is_volatile) override; + void RecordWriteField64(mirror::Object* obj, + MemberOffset field_offset, + uint64_t value, + bool is_volatile) override; + void RecordWriteFieldReference(mirror::Object* obj, + MemberOffset field_offset, + ObjPtr<mirror::Object> value, + bool is_volatile) override + REQUIRES_SHARED(Locks::mutator_lock_); + void RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) override + REQUIRES_SHARED(Locks::mutator_lock_); + void RecordStrongStringInsertion(ObjPtr<mirror::String> s) override + REQUIRES(Locks::intern_table_lock_); + void RecordWeakStringInsertion(ObjPtr<mirror::String> s) override + REQUIRES(Locks::intern_table_lock_); + void RecordStrongStringRemoval(ObjPtr<mirror::String> s) override + REQUIRES(Locks::intern_table_lock_); + void RecordWeakStringRemoval(ObjPtr<mirror::String> s) override + REQUIRES(Locks::intern_table_lock_); + void RecordResolveString(ObjPtr<mirror::DexCache> dex_cache, dex::StringIndex string_idx) override + REQUIRES_SHARED(Locks::mutator_lock_); + void RecordResolveMethodType(ObjPtr<mirror::DexCache> dex_cache, dex::ProtoIndex proto_idx) + override REQUIRES_SHARED(Locks::mutator_lock_); + + // Aborting transactions for AOT compilation. + void ThrowTransactionAbortError(Thread* self) override + REQUIRES_SHARED(Locks::mutator_lock_); + void AbortTransactionF(Thread* self, const char* fmt, ...) override + __attribute__((__format__(__printf__, 3, 4))) REQUIRES_SHARED(Locks::mutator_lock_); - bool TransactionWriteValueConstraint(Thread* self, ObjPtr<mirror::Object> value) const override + void AbortTransactionV(Thread* self, const char* fmt, va_list args) override REQUIRES_SHARED(Locks::mutator_lock_); - bool TransactionAllocationConstraint(Thread* self, ObjPtr<mirror::Class> klass) const override + bool IsTransactionAborted() const override; + + void VisitTransactionRoots(RootVisitor* visitor) override REQUIRES_SHARED(Locks::mutator_lock_); protected: @@ -86,6 +169,12 @@ class AotClassLinker : public ClassLinker { private: std::unique_ptr<SdkChecker> sdk_checker_; + + // Transactions used for pre-initializing classes at compilation time. + // Support nested transactions, maintain a list containing all transactions. Transactions are + // handled under a stack discipline. Because GC needs to go over all transactions, we choose list + // as substantial data structure instead of stack. + std::forward_list<Transaction> preinitialization_transactions_; }; } // namespace art diff --git a/runtime/quick_exception_handler.cc b/runtime/quick_exception_handler.cc index 2ebcbb0fd0..37734f7d74 100644 --- a/runtime/quick_exception_handler.cc +++ b/runtime/quick_exception_handler.cc @@ -439,19 +439,41 @@ class DeoptimizeStackVisitor final : public StackVisitor { ArtMethod* method = GetMethod(); VLOG(deopt) << "Deoptimizing stack: depth: " << GetFrameDepth() << " at method " << ArtMethod::PrettyMethod(method); + if (method == nullptr || single_frame_done_) { FinishStackWalk(); return false; // End stack walk. - } else if (method->IsRuntimeMethod()) { + } + + // Update if method exit event needs to be reported. We should report exit event only if we + // have reported an entry event. So tell interpreter if/ an entry event was reported. + bool supports_exit_events = Runtime::Current()->GetInstrumentation()->MethodSupportsExitEvents( + method, GetCurrentOatQuickMethodHeader()); + + if (method->IsRuntimeMethod()) { // Ignore callee save method. DCHECK(method->IsCalleeSaveMethod()); return true; } else if (method->IsNative()) { // If we return from JNI with a pending exception and want to deoptimize, we need to skip // the native method. The top method is a runtime method, the native method comes next. - // We also deoptimize due to method instrumentation reasons from method entry / exit - // callbacks. In these cases native method is at the top of stack. + // We also deoptimize due to method instrumentation reasons from method exit callbacks. + // In these cases native method is at the top of stack. CHECK((GetFrameDepth() == 1U) || (GetFrameDepth() == 0U)); + // We see a native frame when: + // 1. returning from JNI with a pending exception + // 2. deopting from method exit callbacks (with or without a pending exception). + // skip_method_exit_callbacks_ is set in this case + // 3. handling async exception on suspend points for fast native methods. + // We only need to call method unwind event in the first case. + if (supports_exit_events && + !skip_method_exit_callbacks_ && + GetThread()->IsExceptionPending()) { + // An exception has occurred in a native method and we are deoptimizing past the native + // method. So report method unwind event here. + Runtime::Current()->GetInstrumentation()->MethodUnwindEvent( + GetThread(), method, dex::kDexNoIndex); + } callee_method_ = method; return true; } else if (!single_frame_deopt_ && @@ -482,11 +504,6 @@ class DeoptimizeStackVisitor final : public StackVisitor { } else { HandleOptimizingDeoptimization(method, new_frame, updated_vregs); } - // Update if method exit event needs to be reported. We should report exit event only if we - // have reported an entry event. So tell interpreter if/ an entry event was reported. - bool supports_exit_events = - Runtime::Current()->GetInstrumentation()->MethodSupportsExitEvents( - method, GetCurrentOatQuickMethodHeader()); new_frame->SetSkipMethodExitEvents(!supports_exit_events); // If we are deoptimizing after method exit callback we shouldn't call the method exit // callbacks again for the top frame. We may have to deopt after the callback if the callback diff --git a/runtime/reflection.cc b/runtime/reflection.cc index 31dd8a4e0e..5c394b136a 100644 --- a/runtime/reflection.cc +++ b/runtime/reflection.cc @@ -496,7 +496,8 @@ bool InvokeMethodImpl(const ScopedObjectAccessAlreadyRunnable& soa, if (soa.Self()->IsExceptionPending()) { // To abort a transaction we use a fake exception that should never be caught by the bytecode // and therefore it makes no sense to wrap it. - if (Runtime::Current()->IsTransactionAborted()) { + if (Runtime::Current()->IsActiveTransaction() && + Runtime::Current()->GetClassLinker()->IsTransactionAborted()) { DCHECK(soa.Self()->GetException()->GetClass()->DescriptorEquals( "Ldalvik/system/TransactionAbortError;")) << soa.Self()->GetException()->GetClass()->PrettyDescriptor(); diff --git a/runtime/runtime.cc b/runtime/runtime.cc index bc833b8cfa..bb60a863cd 100644 --- a/runtime/runtime.cc +++ b/runtime/runtime.cc @@ -178,7 +178,6 @@ #include "thread_list.h" #include "ti/agent.h" #include "trace.h" -#include "transaction.h" #include "vdex_file.h" #include "verifier/class_verifier.h" #include "well_known_classes-inl.h" @@ -287,7 +286,7 @@ Runtime::Runtime() system_thread_group_(nullptr), system_class_loader_(nullptr), dump_gc_performance_on_shutdown_(false), - preinitialization_transactions_(), + active_transaction_(false), verify_(verifier::VerifyMode::kNone), target_sdk_version_(static_cast<uint32_t>(SdkVersion::kUnset)), compat_framework_(), @@ -2661,12 +2660,6 @@ void Runtime::VisitConcurrentRoots(RootVisitor* visitor, VisitRootFlags flags) { } } -void Runtime::VisitTransactionRoots(RootVisitor* visitor) { - for (Transaction& transaction : preinitialization_transactions_) { - transaction.VisitRoots(visitor); - } -} - void Runtime::VisitNonThreadRoots(RootVisitor* visitor) { java_vm_->VisitRoots(visitor); sentinel_.VisitRootIfNonNull(visitor, RootInfo(kRootVMInternal)); @@ -2678,7 +2671,7 @@ void Runtime::VisitNonThreadRoots(RootVisitor* visitor) { .VisitRootIfNonNull(visitor, RootInfo(kRootVMInternal)); pre_allocated_NoClassDefFoundError_.VisitRootIfNonNull(visitor, RootInfo(kRootVMInternal)); VisitImageRoots(visitor); - VisitTransactionRoots(visitor); + class_linker_->VisitTransactionRoots(visitor); } void Runtime::VisitNonConcurrentRoots(RootVisitor* visitor, VisitRootFlags flags) { @@ -2931,221 +2924,6 @@ void Runtime::RegisterAppInfo(const std::string& package_name, jit_->StartProfileSaver(profile_output_filename, code_paths, ref_profile_filename); } -// Transaction support. -bool Runtime::IsActiveTransaction() const { - return !preinitialization_transactions_.empty() && !GetTransaction()->IsRollingBack(); -} - -void Runtime::EnterTransactionMode(bool strict, mirror::Class* root) { - DCHECK(IsAotCompiler()); - ArenaPool* arena_pool = nullptr; - ArenaStack* arena_stack = nullptr; - if (preinitialization_transactions_.empty()) { // Top-level transaction? - // Make initialized classes visibly initialized now. If that happened during the transaction - // and then the transaction was aborted, we would roll back the status update but not the - // ClassLinker's bookkeeping structures, so these classes would never be visibly initialized. - { - Thread* self = Thread::Current(); - StackHandleScope<1> hs(self); - HandleWrapper<mirror::Class> h(hs.NewHandleWrapper(&root)); - ScopedThreadSuspension sts(self, ThreadState::kNative); - GetClassLinker()->MakeInitializedClassesVisiblyInitialized(Thread::Current(), /*wait=*/ true); - } - // Pass the runtime `ArenaPool` to the transaction. - arena_pool = GetArenaPool(); - } else { - // Pass the `ArenaStack` from previous transaction to the new one. - arena_stack = preinitialization_transactions_.front().GetArenaStack(); - } - preinitialization_transactions_.emplace_front(strict, root, arena_stack, arena_pool); -} - -void Runtime::ExitTransactionMode() { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - preinitialization_transactions_.pop_front(); -} - -void Runtime::RollbackAndExitTransactionMode() { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - preinitialization_transactions_.front().Rollback(); - preinitialization_transactions_.pop_front(); -} - -bool Runtime::IsTransactionAborted() const { - if (!IsActiveTransaction()) { - return false; - } else { - DCHECK(IsAotCompiler()); - return GetTransaction()->IsAborted(); - } -} - -void Runtime::RollbackAllTransactions() { - // If transaction is aborted, all transactions will be kept in the list. - // Rollback and exit all of them. - while (IsActiveTransaction()) { - RollbackAndExitTransactionMode(); - } -} - -bool Runtime::IsActiveStrictTransactionMode() const { - return IsActiveTransaction() && GetTransaction()->IsStrict(); -} - -const Transaction* Runtime::GetTransaction() const { - DCHECK(!preinitialization_transactions_.empty()); - return &preinitialization_transactions_.front(); -} - -Transaction* Runtime::GetTransaction() { - DCHECK(!preinitialization_transactions_.empty()); - return &preinitialization_transactions_.front(); -} - -void Runtime::AbortTransactionAndThrowAbortError(Thread* self, const std::string& abort_message) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - // Throwing an exception may cause its class initialization. If we mark the transaction - // aborted before that, we may warn with a false alarm. Throwing the exception before - // marking the transaction aborted avoids that. - // But now the transaction can be nested, and abort the transaction will relax the constraints - // for constructing stack trace. - GetTransaction()->Abort(abort_message); - GetTransaction()->ThrowAbortError(self, &abort_message); -} - -void Runtime::ThrowTransactionAbortError(Thread* self) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - // Passing nullptr means we rethrow an exception with the earlier transaction abort message. - GetTransaction()->ThrowAbortError(self, nullptr); -} - -void Runtime::AbortTransactionF(Thread* self, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - AbortTransactionV(self, fmt, args); - va_end(args); -} - -void Runtime::AbortTransactionV(Thread* self, const char* fmt, va_list args) { - CHECK(IsActiveTransaction()); - // Constructs abort message. - std::string abort_msg; - android::base::StringAppendV(&abort_msg, fmt, args); - // Throws an exception so we can abort the transaction and rollback every change. - AbortTransactionAndThrowAbortError(self, abort_msg); -} - -void Runtime::RecordWriteFieldBoolean(mirror::Object* obj, - MemberOffset field_offset, - uint8_t value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteFieldBoolean(obj, field_offset, value, is_volatile); -} - -void Runtime::RecordWriteFieldByte(mirror::Object* obj, - MemberOffset field_offset, - int8_t value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteFieldByte(obj, field_offset, value, is_volatile); -} - -void Runtime::RecordWriteFieldChar(mirror::Object* obj, - MemberOffset field_offset, - uint16_t value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteFieldChar(obj, field_offset, value, is_volatile); -} - -void Runtime::RecordWriteFieldShort(mirror::Object* obj, - MemberOffset field_offset, - int16_t value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteFieldShort(obj, field_offset, value, is_volatile); -} - -void Runtime::RecordWriteField32(mirror::Object* obj, - MemberOffset field_offset, - uint32_t value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteField32(obj, field_offset, value, is_volatile); -} - -void Runtime::RecordWriteField64(mirror::Object* obj, - MemberOffset field_offset, - uint64_t value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteField64(obj, field_offset, value, is_volatile); -} - -void Runtime::RecordWriteFieldReference(mirror::Object* obj, - MemberOffset field_offset, - ObjPtr<mirror::Object> value, - bool is_volatile) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteFieldReference(obj, field_offset, value.Ptr(), is_volatile); -} - -void Runtime::RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWriteArray(array, index, value); -} - -void Runtime::RecordStrongStringInsertion(ObjPtr<mirror::String> s) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordStrongStringInsertion(s); -} - -void Runtime::RecordWeakStringInsertion(ObjPtr<mirror::String> s) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWeakStringInsertion(s); -} - -void Runtime::RecordStrongStringRemoval(ObjPtr<mirror::String> s) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordStrongStringRemoval(s); -} - -void Runtime::RecordWeakStringRemoval(ObjPtr<mirror::String> s) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordWeakStringRemoval(s); -} - -void Runtime::RecordResolveString(ObjPtr<mirror::DexCache> dex_cache, - dex::StringIndex string_idx) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordResolveString(dex_cache, string_idx); -} - -void Runtime::RecordResolveMethodType(ObjPtr<mirror::DexCache> dex_cache, - dex::ProtoIndex proto_idx) { - DCHECK(IsAotCompiler()); - DCHECK(IsActiveTransaction()); - GetTransaction()->RecordResolveMethodType(dex_cache, proto_idx); -} - void Runtime::SetFaultMessage(const std::string& message) { std::string* new_msg = new std::string(message); std::string* cur_msg = fault_message_.exchange(new_msg); diff --git a/runtime/runtime.h b/runtime/runtime.h index a48bb55755..96f76652d2 100644 --- a/runtime/runtime.h +++ b/runtime/runtime.h @@ -20,7 +20,6 @@ #include <jni.h> #include <stdio.h> -#include <forward_list> #include <iosfwd> #include <memory> #include <optional> @@ -120,7 +119,6 @@ class ThreadList; class ThreadPool; class Trace; struct TraceConfig; -class Transaction; using RuntimeOptions = std::vector<std::pair<std::string, const void*>>; @@ -482,9 +480,6 @@ class Runtime { void VisitNonThreadRoots(RootVisitor* visitor) REQUIRES_SHARED(Locks::mutator_lock_); - void VisitTransactionRoots(RootVisitor* visitor) - REQUIRES_SHARED(Locks::mutator_lock_); - // Sweep system weaks, the system weak is deleted if the visitor return null. Otherwise, the // system weak is updated to be the visitor's returned value. EXPORT void SweepSystemWeaks(IsMarkedVisitor* visitor) REQUIRES_SHARED(Locks::mutator_lock_); @@ -626,75 +621,19 @@ class Runtime { const std::string& ref_profile_filename, int32_t code_type); - // Transaction support. - EXPORT bool IsActiveTransaction() const; - // EnterTransactionMode may suspend. - EXPORT void EnterTransactionMode(bool strict, mirror::Class* root) - REQUIRES_SHARED(Locks::mutator_lock_); - EXPORT void ExitTransactionMode(); - EXPORT void RollbackAllTransactions() REQUIRES_SHARED(Locks::mutator_lock_); - // Transaction rollback and exit transaction are always done together, it's convenience to - // do them in one function. - void RollbackAndExitTransactionMode() REQUIRES_SHARED(Locks::mutator_lock_); - bool IsTransactionAborted() const; - const Transaction* GetTransaction() const; - Transaction* GetTransaction(); - bool IsActiveStrictTransactionMode() const; - - void AbortTransactionAndThrowAbortError(Thread* self, const std::string& abort_message) - REQUIRES_SHARED(Locks::mutator_lock_); - void ThrowTransactionAbortError(Thread* self) - REQUIRES_SHARED(Locks::mutator_lock_); - - void AbortTransactionF(Thread* self, const char* fmt, ...) - __attribute__((__format__(__printf__, 3, 4))) - REQUIRES_SHARED(Locks::mutator_lock_); + void SetActiveTransaction() { + DCHECK(IsAotCompiler()); + active_transaction_ = true; + } - void AbortTransactionV(Thread* self, const char* fmt, va_list args) - REQUIRES_SHARED(Locks::mutator_lock_); + void ClearActiveTransaction() { + DCHECK(IsAotCompiler()); + active_transaction_ = false; + } - void RecordWriteFieldBoolean(mirror::Object* obj, - MemberOffset field_offset, - uint8_t value, - bool is_volatile); - void RecordWriteFieldByte(mirror::Object* obj, - MemberOffset field_offset, - int8_t value, - bool is_volatile); - void RecordWriteFieldChar(mirror::Object* obj, - MemberOffset field_offset, - uint16_t value, - bool is_volatile); - void RecordWriteFieldShort(mirror::Object* obj, - MemberOffset field_offset, - int16_t value, - bool is_volatile); - EXPORT void RecordWriteField32(mirror::Object* obj, - MemberOffset field_offset, - uint32_t value, - bool is_volatile); - void RecordWriteField64(mirror::Object* obj, - MemberOffset field_offset, - uint64_t value, - bool is_volatile); - EXPORT void RecordWriteFieldReference(mirror::Object* obj, - MemberOffset field_offset, - ObjPtr<mirror::Object> value, - bool is_volatile) REQUIRES_SHARED(Locks::mutator_lock_); - EXPORT void RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) - REQUIRES_SHARED(Locks::mutator_lock_); - void RecordStrongStringInsertion(ObjPtr<mirror::String> s) - REQUIRES(Locks::intern_table_lock_); - void RecordWeakStringInsertion(ObjPtr<mirror::String> s) - REQUIRES(Locks::intern_table_lock_); - void RecordStrongStringRemoval(ObjPtr<mirror::String> s) - REQUIRES(Locks::intern_table_lock_); - void RecordWeakStringRemoval(ObjPtr<mirror::String> s) - REQUIRES(Locks::intern_table_lock_); - void RecordResolveString(ObjPtr<mirror::DexCache> dex_cache, dex::StringIndex string_idx) - REQUIRES_SHARED(Locks::mutator_lock_); - void RecordResolveMethodType(ObjPtr<mirror::DexCache> dex_cache, dex::ProtoIndex proto_idx) - REQUIRES_SHARED(Locks::mutator_lock_); + bool IsActiveTransaction() { + return active_transaction_; + } void SetFaultMessage(const std::string& message); @@ -1401,11 +1340,12 @@ class Runtime { // If true, then we dump the GC cumulative timings on shutdown. bool dump_gc_performance_on_shutdown_; - // Transactions used for pre-initializing classes at compilation time. - // Support nested transactions, maintain a list containing all transactions. Transactions are - // handled under a stack discipline. Because GC needs to go over all transactions, we choose list - // as substantial data structure instead of stack. - std::forward_list<Transaction> preinitialization_transactions_; + // Transactions are handled by the `AotClassLinker` but we keep a simple flag + // in the `Runtime` for quick transaction checks. + // Code that's not AOT-specific but needs some transaction-specific behavior + // shall check this flag and, for an active transaction, make virtual calls + // through `ClassLinker` to `AotClassLinker` to implement that behavior. + bool active_transaction_; // If kNone, verification is disabled. kEnable by default. verifier::VerifyMode verify_; diff --git a/runtime/runtime_callbacks.cc b/runtime/runtime_callbacks.cc index aad943020d..0b10d2ee1d 100644 --- a/runtime/runtime_callbacks.cc +++ b/runtime/runtime_callbacks.cc @@ -58,12 +58,52 @@ void RuntimeCallbacks::RemoveDdmCallback(DdmCallback* cb) { Remove(cb, &ddm_callbacks_); } +void RuntimeCallbacks::AddAppInfoCallback(AppInfoCallback* cb) { + WriterMutexLock mu(Thread::Current(), *callback_lock_); + appinfo_callbacks_.push_back(cb); +} + +void RuntimeCallbacks::RemoveAppInfoCallback(AppInfoCallback* cb) { + WriterMutexLock mu(Thread::Current(), *callback_lock_); + Remove(cb, &appinfo_callbacks_); +} + void RuntimeCallbacks::DdmPublishChunk(uint32_t type, const ArrayRef<const uint8_t>& data) { for (DdmCallback* cb : COPY(ddm_callbacks_)) { cb->DdmPublishChunk(type, data); } } +void RuntimeCallbacks::SetCurrentProcessName(const std::string& process_name) { + for (AppInfoCallback* cb : COPY(appinfo_callbacks_)) { + cb->SetCurrentProcessName(process_name); + } +} + +void RuntimeCallbacks::AddApplication(const std::string& package_name) { + for (AppInfoCallback* cb : COPY(appinfo_callbacks_)) { + cb->AddApplication(package_name); + } +} + +void RuntimeCallbacks::RemoveApplication(const std::string& package_name) { + for (AppInfoCallback* cb : COPY(appinfo_callbacks_)) { + cb->RemoveApplication(package_name); + } +} + +void RuntimeCallbacks::SetWaitingForDebugger(bool waiting) { + for (AppInfoCallback* cb : COPY(appinfo_callbacks_)) { + cb->SetWaitingForDebugger(waiting); + } +} + +void RuntimeCallbacks::SetUserId(int user_id) { + for (AppInfoCallback* cb : COPY(appinfo_callbacks_)) { + cb->SetUserId(user_id); + } +} + void RuntimeCallbacks::AddDebuggerControlCallback(DebuggerControlCallback* cb) { WriterMutexLock mu(Thread::Current(), *callback_lock_); debugger_control_callbacks_.push_back(cb); diff --git a/runtime/runtime_callbacks.h b/runtime/runtime_callbacks.h index 9d7e199bfa..4e355151da 100644 --- a/runtime/runtime_callbacks.h +++ b/runtime/runtime_callbacks.h @@ -70,6 +70,19 @@ class DdmCallback { REQUIRES_SHARED(Locks::mutator_lock_) = 0; }; +class AppInfoCallback { + public: + virtual ~AppInfoCallback() {} + virtual void SetCurrentProcessName(const std::string& process_name) + REQUIRES_SHARED(Locks::mutator_lock_) = 0; + virtual void AddApplication(const std::string& package_name) + REQUIRES_SHARED(Locks::mutator_lock_) = 0; + virtual void RemoveApplication(const std::string& package_name) + REQUIRES_SHARED(Locks::mutator_lock_) = 0; + virtual void SetWaitingForDebugger(bool waiting) REQUIRES_SHARED(Locks::mutator_lock_) = 0; + virtual void SetUserId(int uid) REQUIRES_SHARED(Locks::mutator_lock_) = 0; +}; + class DebuggerControlCallback { public: virtual ~DebuggerControlCallback() {} @@ -240,6 +253,15 @@ class EXPORT RuntimeCallbacks { void AddDdmCallback(DdmCallback* cb) REQUIRES_SHARED(Locks::mutator_lock_); void RemoveDdmCallback(DdmCallback* cb) REQUIRES_SHARED(Locks::mutator_lock_); + void SetCurrentProcessName(const std::string& process_name) REQUIRES_SHARED(Locks::mutator_lock_); + void AddApplication(const std::string& package_name) REQUIRES_SHARED(Locks::mutator_lock_); + void RemoveApplication(const std::string& package_name) REQUIRES_SHARED(Locks::mutator_lock_); + void SetWaitingForDebugger(bool waiting) REQUIRES_SHARED(Locks::mutator_lock_); + void SetUserId(int uid) REQUIRES_SHARED(Locks::mutator_lock_); + + void AddAppInfoCallback(AppInfoCallback* cb) REQUIRES_SHARED(Locks::mutator_lock_); + void RemoveAppInfoCallback(AppInfoCallback* cb) REQUIRES_SHARED(Locks::mutator_lock_); + void StartDebugger() REQUIRES_SHARED(Locks::mutator_lock_); // NO_THREAD_SAFETY_ANALYSIS since this is only called when we are in the middle of shutting down // and the mutator_lock_ is no longer acquirable. @@ -279,6 +301,7 @@ class EXPORT RuntimeCallbacks { GUARDED_BY(callback_lock_); std::vector<DdmCallback*> ddm_callbacks_ GUARDED_BY(callback_lock_); + std::vector<AppInfoCallback*> appinfo_callbacks_ GUARDED_BY(callback_lock_); std::vector<DebuggerControlCallback*> debugger_control_callbacks_ GUARDED_BY(callback_lock_); std::vector<ReflectiveValueVisitCallback*> reflective_value_visit_callbacks_ diff --git a/runtime/runtime_image.cc b/runtime/runtime_image.cc index 997ea2fde6..b134c79286 100644 --- a/runtime/runtime_image.cc +++ b/runtime/runtime_image.cc @@ -964,7 +964,9 @@ class RuntimeImageHelper { entrypoint = boot_jni_stub; } } - copy->SetEntryPointFromQuickCompiledCode(entrypoint); + copy->SetNativePointer(ArtMethod::EntryPointFromQuickCompiledCodeOffset(kRuntimePointerSize), + entrypoint, + kRuntimePointerSize); if (method->IsNative()) { StubType stub_type = method->IsCriticalNative() diff --git a/runtime/signal_catcher.cc b/runtime/signal_catcher.cc index 3660ef3a19..09a4b2d85e 100644 --- a/runtime/signal_catcher.cc +++ b/runtime/signal_catcher.cc @@ -16,8 +16,8 @@ #include "signal_catcher.h" -#include <csignal> -#include <cstdlib> +#include <android-base/file.h> +#include <android-base/stringprintf.h> #include <fcntl.h> #include <pthread.h> #include <sys/stat.h> @@ -25,13 +25,13 @@ #include <sys/types.h> #include <unistd.h> +#include <csignal> +#include <cstdlib> #include <optional> #include <sstream> -#include <android-base/file.h> -#include <android-base/stringprintf.h> - #include "arch/instruction_set.h" +#include "base/debugstore.h" #include "base/logging.h" // For GetCmdLine. #include "base/os.h" #include "base/time_utils.h" @@ -136,6 +136,8 @@ void SignalCatcher::HandleSigQuit() { os << "Build type: " << (kIsDebugBuild ? "debug" : "optimized") << "\n"; + os << "Debug Store: " << DebugStoreGetString() << "\n"; + runtime->DumpForSigQuit(os); if ((false)) { diff --git a/runtime/startup_completed_task.cc b/runtime/startup_completed_task.cc index fa768d52df..b84c3107f3 100644 --- a/runtime/startup_completed_task.cc +++ b/runtime/startup_completed_task.cc @@ -53,11 +53,12 @@ void StartupCompletedTask::Run(Thread* self) { if (!runtime->IsJavaDebuggable()) { std::string compiler_filter; std::string compilation_reason; + std::string primary_apk_path = runtime->GetAppInfo()->GetPrimaryApkPath(); runtime->GetAppInfo()->GetPrimaryApkOptimizationStatus(&compiler_filter, &compilation_reason); CompilerFilter::Filter filter; if (CompilerFilter::ParseCompilerFilter(compiler_filter.c_str(), &filter) && !CompilerFilter::IsAotCompilationEnabled(filter) && - !runtime->GetHeap()->HasAppImageSpace()) { + !runtime->GetHeap()->HasAppImageSpaceFor(primary_apk_path)) { std::string error_msg; if (!RuntimeImage::WriteImageToDisk(&error_msg)) { LOG(DEBUG) << "Could not write temporary image to disk " << error_msg; diff --git a/runtime/thread_list.cc b/runtime/thread_list.cc index 436daa0b54..4791b7c072 100644 --- a/runtime/thread_list.cc +++ b/runtime/thread_list.cc @@ -1488,6 +1488,19 @@ void ThreadList::Unregister(Thread* self, bool should_run_callbacks) { usleep(1); // We failed to remove the thread due to a suspend request or the like, loop and try again. } + + // We flush the trace buffer in Thread::Destroy. We have to check again here because once the + // Thread::Destroy finishes we wait for any active suspend requests to finish before deleting + // the thread. If a new trace was started during the wait period we may allocate the trace buffer + // again. The trace buffer would only contain the method entry events for the methods on the stack + // of an exiting thread. It is not required to flush these entries but we need to release the + // buffer. Ideally we should either not generate trace events for a thread that is exiting or use + // a different mechanism to report the initial events on a trace start that doesn't use per-thread + // buffer. Both these approaches are not trivial to implement, so we are going with the approach + // of just releasing the buffer here. + if (UNLIKELY(self->GetMethodTraceBuffer() != nullptr)) { + Trace::ReleaseThreadBuffer(self); + } delete self; // Release the thread ID after the thread is finished and deleted to avoid cases where we can diff --git a/runtime/thread_pool.cc b/runtime/thread_pool.cc index f0ea078cdc..80fd3587ae 100644 --- a/runtime/thread_pool.cc +++ b/runtime/thread_pool.cc @@ -86,14 +86,22 @@ ThreadPoolWorker::~ThreadPoolWorker() { CHECK_PTHREAD_CALL(pthread_join, (pthread_, nullptr), "thread pool worker shutdown"); } -void ThreadPoolWorker::SetPthreadPriority(int priority) { +// Set the "nice" priority for tid (0 means self). +static void SetPriorityForTid(pid_t tid, int priority) { CHECK_GE(priority, PRIO_MIN); CHECK_LE(priority, PRIO_MAX); -#if defined(ART_TARGET_ANDROID) - int result = setpriority(PRIO_PROCESS, pthread_gettid_np(pthread_), priority); + int result = setpriority(PRIO_PROCESS, tid, priority); if (result != 0) { - PLOG(ERROR) << "Failed to setpriority to :" << priority; +#if defined(ART_TARGET_ANDROID) + PLOG(WARNING) << "Failed to setpriority to :" << priority; +#endif + // Setpriority may fail on host due to ulimit issues. } +} + +void ThreadPoolWorker::SetPthreadPriority(int priority) { +#if defined(ART_TARGET_ANDROID) + SetPriorityForTid(pthread_gettid_np(pthread_), priority); #else UNUSED(priority); #endif @@ -144,6 +152,10 @@ void* ThreadPoolWorker::Callback(void* arg) { // Do work until its time to shut down. worker->Run(); runtime->DetachCurrentThread(/* should_run_callbacks= */ false); + // On zygote fork, we wait for this thread to exit completely. Set to highest Java priority + // to speed that up. + constexpr int kJavaMaxPrioNiceness = -8; + SetPriorityForTid(0 /* this thread */, kJavaMaxPrioNiceness); return nullptr; } diff --git a/runtime/trace.cc b/runtime/trace.cc index e2fbd65bbe..71dd74f01b 100644 --- a/runtime/trace.cc +++ b/runtime/trace.cc @@ -271,8 +271,7 @@ class TraceWriterTask final : public SelfDeletingTask { index_(index), buffer_(buffer), cur_offset_(cur_offset), - thread_id_(thread_id), - reserve_buf_for_tid_(0) {} + thread_id_(thread_id) {} void Run(Thread* self ATTRIBUTE_UNUSED) override { std::unordered_map<ArtMethod*, std::string> method_infos; @@ -282,23 +281,12 @@ class TraceWriterTask final : public SelfDeletingTask { } trace_writer_->FlushBuffer(buffer_, cur_offset_, thread_id_, method_infos); if (index_ == -1) { - // This was a temporary buffer we allocated since there are no more free buffers and we - // couldn't find one by flushing the pending tasks either. This should only happen when we - // have fewer buffers than the number of threads. - if (reserve_buf_for_tid_ == 0) { - // Just free the buffer here if it wasn't reserved for any thread. - delete[] buffer_; - } - } else { - trace_writer_->FetchTraceBufferForThread(index_, reserve_buf_for_tid_); + // This was a temporary buffer we allocated since there are no free buffers and it wasn't + // safe to wait for one. This should only happen when we have fewer buffers than the number + // of threads. + delete[] buffer_; } - } - - // Reserves the buffer for a particular thread. The thread is free to use this buffer once the - // task has finished running. This is used when there are no free buffers for the thread to use. - uintptr_t* ReserveBufferForTid(size_t tid) { - reserve_buf_for_tid_ = tid; - return buffer_; + trace_writer_->ReleaseBuffer(index_); } private: @@ -307,10 +295,6 @@ class TraceWriterTask final : public SelfDeletingTask { uintptr_t* buffer_; size_t cur_offset_; size_t thread_id_; - // Sometimes we want to acquire a buffer for a particular thread. This holds - // the tid of the thread that we want to acquire the buffer for. If this value - // is 0 then it means we can use it for other threads. - size_t reserve_buf_for_tid_; }; std::vector<ArtMethod*>* Trace::AllocStackTrace() { @@ -750,6 +734,9 @@ void Trace::StopTracing(bool flush_entries) { CHECK_PTHREAD_CALL(pthread_join, (sampling_pthread, nullptr), "sampling thread shutdown"); } + // Wakeup any threads waiting for a buffer and abort allocating a buffer. + the_trace_->trace_writer_->StopTracing(); + // Make a copy of the_trace_, so it can be flushed later. We want to reset // the_trace_ to nullptr in suspend all scope to prevent any races Trace* the_trace = the_trace_; @@ -837,6 +824,17 @@ void Trace::FlushThreadBuffer(Thread* self) { the_trace_->trace_writer_->FlushBuffer(self, /* is_sync= */ false, /* free_buffer= */ true); } +void Trace::ReleaseThreadBuffer(Thread* self) { + MutexLock mu(self, *Locks::trace_lock_); + // Check if we still need to flush inside the trace_lock_. If we are stopping tracing it is + // possible we already deleted the trace and flushed the buffer too. + if (the_trace_ == nullptr) { + DCHECK_EQ(self->GetMethodTraceBuffer(), nullptr); + return; + } + the_trace_->trace_writer_->ReleaseBufferForThread(self); +} + void Trace::Abort() { // Do not write anything anymore. StopTracing(/* flush_entries= */ false); @@ -919,6 +917,10 @@ TraceWriter::TraceWriter(File* trace_file, num_records_(0), clock_overhead_ns_(clock_overhead_ns), owner_tids_(num_trace_buffers), + buffer_pool_lock_("tracing buffer pool lock", kDefaultMutexLevel), + buffer_available_("buffer available condition", buffer_pool_lock_), + num_waiters_zero_cond_("Num waiters zero", buffer_pool_lock_), + num_waiters_for_buffer_(0), tracing_lock_("tracing lock", LockLevel::kTracingStreamingLock) { // We initialize the start_time_ from the timestamp counter. This may not match // with the monotonic timer but we only use this time to calculate the elapsed @@ -1014,8 +1016,9 @@ void TraceWriter::FinishTracing(int flags, bool flush_entries) { // shutdown, any unstarted workers can create problems if they try attaching while shutting // down. thread_pool_->WaitForWorkersToBeCreated(); - // Wait for any outstanding writer tasks to finish. - thread_pool_->Wait(self, /* do_work= */ true, /* may_hold_locks= */ true); + // Wait for any outstanding writer tasks to finish. Let the thread pool worker finish the + // tasks to avoid any re-ordering when processing tasks. + thread_pool_->Wait(self, /* do_work= */ false, /* may_hold_locks= */ true); DCHECK_EQ(thread_pool_->GetTaskCount(self), 0u); thread_pool_->StopWorkers(self); } @@ -1225,24 +1228,6 @@ void Trace::ReadClocks(Thread* thread, uint32_t* thread_clock_diff, uint64_t* ti } } -uintptr_t* TraceWriterThreadPool::FinishTaskAndClaimBuffer(size_t tid) { - Thread* self = Thread::Current(); - TraceWriterTask* task = static_cast<TraceWriterTask*>(TryGetTask(self)); - if (task == nullptr) { - // TODO(mythria): We need to ensure we have at least as many buffers in the pool as the number - // of active threads for efficiency. It's a bit unlikely to hit this case and not trivial to - // handle this. So we haven't fixed this yet. - LOG(WARNING) - << "Fewer buffers in the pool than the number of threads. Might cause some slowdown"; - return nullptr; - } - - uintptr_t* buffer = task->ReserveBufferForTid(tid); - task->Run(self); - task->Finalize(); - return buffer; -} - std::string TraceWriter::GetMethodLine(const std::string& method_line, uint32_t method_index) { return StringPrintf("%#x\t%s", (method_index << TraceActionBits), method_line.c_str()); } @@ -1399,6 +1384,9 @@ void TraceWriter::InitializeTraceBuffers() { } uintptr_t* TraceWriter::AcquireTraceBuffer(size_t tid) { + Thread* self = Thread::Current(); + + // Fast path, check if there is a free buffer in the pool for (size_t index = 0; index < owner_tids_.size(); index++) { size_t owner = 0; if (owner_tids_[index].compare_exchange_strong(owner, tid)) { @@ -1406,22 +1394,101 @@ uintptr_t* TraceWriter::AcquireTraceBuffer(size_t tid) { } } - // No free buffers, flush the buffer at the start of task queue synchronously and then use that - // buffer. - uintptr_t* buffer = thread_pool_->FinishTaskAndClaimBuffer(tid); + // Increment a counter so we know how many threads are potentially suspended in the tracing code. + // We need this when stopping tracing. We need to wait for all these threads to finish executing + // this code so we can safely delete the trace related data. + num_waiters_for_buffer_.fetch_add(1); + + uintptr_t* buffer = nullptr; + // If finish_tracing_ is set to true we shouldn't suspend ourselves. So check for finish_tracing_ + // before the thread suspension. As an example, consider the following: + // T2 is looking for a free buffer in the loop above + // T1 calls stop tracing -> Sets finish_tracing_ to true -> Checks that there are no waiters -> + // Waiting to suspend all threads. + // T2 doesn't find a buffer. + // If T2 suspends before checking for finish_tracing_ there is a possibility T1 succeeds entering + // SuspendAllScope while thread T2 is still in the TraceWriter code. + // To avoid this, we increment the num_waiters_for_buffer and then check for finish_tracing + // before suspending the thread. StopTracing sets finish_tracing_ to true first and then checks + // for num_waiters_for_buffer. Both these are atomic variables and we use sequential consistency + // (acquire for load and release for stores), so all threads see the updates for these variables + // in the same order. That ensures we don't suspend in the tracing logic after Trace::StopTracing + // has returned. This is required so that we can safely delete tracing data. + if (self->IsThreadSuspensionAllowable() && !finish_tracing_.load()) { + ScopedThreadSuspension sts(self, ThreadState::kSuspended); + while (1) { + MutexLock mu(self, buffer_pool_lock_); + // Tracing is being stopped, so don't wait for a free buffer. Just return early. + if (finish_tracing_.load()) { + break; + } + + // Check if there's a free buffer in the pool + for (size_t index = 0; index < owner_tids_.size(); index++) { + size_t owner = 0; + if (owner_tids_[index].compare_exchange_strong(owner, tid)) { + buffer = trace_buffer_.get() + index * kPerThreadBufSize; + break; + } + } + + // Found a buffer + if (buffer != nullptr) { + break; + } + + if (thread_pool_ == nullptr || + (thread_pool_->GetTaskCount(self) < num_waiters_for_buffer_.load())) { + // We have fewer buffers than active threads, just allocate a new one. + break; + } + + buffer_available_.WaitHoldingLocks(self); + } + } + + // The thread is no longer in the suspend scope, so decrement the counter. + num_waiters_for_buffer_.fetch_sub(1); + if (num_waiters_for_buffer_.load() == 0 && finish_tracing_.load()) { + MutexLock mu(self, buffer_pool_lock_); + num_waiters_zero_cond_.Broadcast(self); + } + if (buffer == nullptr) { - // We couldn't find a free buffer even after flushing all the tasks. So allocate a new buffer - // here. This should only happen if we have more threads than the number of pool buffers. - // TODO(mythria): Add a check for the above case here. + // Allocate a new buffer. We either don't want to wait or have too few buffers. buffer = new uintptr_t[kPerThreadBufSize]; CHECK(buffer != nullptr); } return buffer; } -void TraceWriter::FetchTraceBufferForThread(int index, size_t tid) { +void TraceWriter::StopTracing() { + Thread* self = Thread::Current(); + MutexLock mu(self, buffer_pool_lock_); + finish_tracing_.store(true); + while (num_waiters_for_buffer_.load() != 0) { + buffer_available_.Broadcast(self); + num_waiters_zero_cond_.WaitHoldingLocks(self); + } +} + +void TraceWriter::ReleaseBuffer(int index) { // Only the trace_writer_ thread can release the buffer. - owner_tids_[index].store(tid); + MutexLock mu(Thread::Current(), buffer_pool_lock_); + if (index != -1) { + owner_tids_[index].store(0); + } + buffer_available_.Signal(Thread::Current()); +} + +void TraceWriter::ReleaseBufferForThread(Thread* self) { + uintptr_t* buffer = self->GetMethodTraceBuffer(); + int index = GetMethodTraceIndex(buffer); + if (index == -1) { + delete[] buffer; + } else { + ReleaseBuffer(index); + } } int TraceWriter::GetMethodTraceIndex(uintptr_t* current_buffer) { diff --git a/runtime/trace.h b/runtime/trace.h index b2012ed8e6..c3d3b66cd3 100644 --- a/runtime/trace.h +++ b/runtime/trace.h @@ -204,13 +204,19 @@ class TraceWriter { // as the owner tid. This also allocates the buffer pool. void InitializeTraceBuffers(); - // Releases the trace buffer and transfers the ownership to the specified tid. If the tid is 0, - // then it means it is free and other threads can claim it. - void FetchTraceBufferForThread(int index, size_t tid); + // Releases the trace buffer and signals any waiting threads about a free buffer. + void ReleaseBuffer(int index); + + // Release the trace buffer of the thread. This is called to release the buffer without flushing + // the entries. See a comment in ThreadList::Unregister for more detailed explanation. + void ReleaseBufferForThread(Thread* self); // Tries to find a free buffer (which has owner of 0) from the pool. If there are no free buffers - // it fetches a task, flushes the contents of the buffer and returns that buffer. - uintptr_t* AcquireTraceBuffer(size_t tid); + // then it just waits for a free buffer. To prevent any deadlocks, we only wait if the number of + // pending tasks are greater than the number of waiting threads. Allocates a new buffer if it + // isn't safe to wait. + uintptr_t* AcquireTraceBuffer(size_t tid) REQUIRES_SHARED(Locks::mutator_lock_) + REQUIRES(!tracing_lock_); // Returns the index corresponding to the start of the current_buffer. We allocate one large // buffer and assign parts of it for each thread. @@ -218,6 +224,10 @@ class TraceWriter { int GetTraceFormatVersion() { return trace_format_version_; } + // Ensures that there are no threads suspended waiting for a free buffer. It signals threads + // waiting for a free buffer and waits for all the threads to respond to the signal. + void StopTracing(); + private: void ReadValuesFromRecord(uintptr_t* method_trace_entries, size_t record_index, @@ -345,6 +355,12 @@ class TraceWriter { std::vector<std::atomic<size_t>> owner_tids_; std::unique_ptr<uintptr_t[]> trace_buffer_; + Mutex buffer_pool_lock_; + ConditionVariable buffer_available_ GUARDED_BY(buffer_pool_lock_); + ConditionVariable num_waiters_zero_cond_ GUARDED_BY(buffer_pool_lock_); + std::atomic<size_t> num_waiters_for_buffer_; + std::atomic<bool> finish_tracing_ = false; + // Lock to protect common data structures accessed from multiple threads like // art_method_id_map_, thread_id_map_. Mutex tracing_lock_; @@ -422,6 +438,11 @@ class Trace final : public instrumentation::InstrumentationListener, public Clas static void FlushThreadBuffer(Thread* thread) REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(!Locks::trace_lock_) NO_THREAD_SAFETY_ANALYSIS; + // Release per-thread buffer without flushing any entries. This is used when a new trace buffer is + // allocated while the thread is terminating. See ThreadList::Unregister for more details. + static void ReleaseThreadBuffer(Thread* thread) + REQUIRES(!Locks::trace_lock_) NO_THREAD_SAFETY_ANALYSIS; + // Removes any listeners installed for method tracing. This is used in non-streaming case // when we no longer record any events once the buffer is full. In other cases listeners are // removed only when tracing stops. This is expected to be called in SuspendAll scope. diff --git a/runtime/transaction.cc b/runtime/transaction.cc index 9a8fcd9739..9b3985503a 100644 --- a/runtime/transaction.cc +++ b/runtime/transaction.cc @@ -57,6 +57,7 @@ Transaction::Transaction(bool strict, heap_(Runtime::Current()->GetHeap()), strict_(strict), root_(root), + last_allocated_object_(nullptr), assert_no_new_records_reason_(nullptr) { DCHECK(Runtime::Current()->IsAotCompiler()); DCHECK_NE(arena_stack != nullptr, arena_pool != nullptr); @@ -164,6 +165,42 @@ bool Transaction::ReadConstraint(ObjPtr<mirror::Object> obj) const { } } +void Transaction::RecordNewObject(ObjPtr<mirror::Object> obj) { + last_allocated_object_ = obj.Ptr(); + ObjectLog log(&allocator_); + log.MarkAsNewObject(); + object_logs_.Put(obj.Ptr(), std::move(log)); +} + +void Transaction::RecordNewArray(ObjPtr<mirror::Array> array) { + if (array->IsObjectArray()) { + // `ObjectArray<T>::SetWithoutChecks()` uses `SetFieldObject()` which records value + // changes in `object_log_`, so we need to record new object arrays as normal objects. + RecordNewObject(array); + return; + } + last_allocated_object_ = array.Ptr(); + ArrayLog log(&allocator_); + log.MarkAsNewArray(); + array_logs_.Put(array.Ptr(), std::move(log)); +} + +bool Transaction::ObjectNeedsTransactionRecords(ObjPtr<mirror::Object> obj) { + if (obj == last_allocated_object_) { + return false; + } + auto it = object_logs_.find(obj.Ptr()); + return it == object_logs_.end() || !it->second.IsNewObject(); +} + +bool Transaction::ArrayNeedsTransactionRecords(ObjPtr<mirror::Array> array) { + if (array == last_allocated_object_) { + return false; + } + auto it = array_logs_.find(array.Ptr()); + return it == array_logs_.end() || !it->second.IsNewArray(); +} + inline Transaction::ObjectLog& Transaction::GetOrCreateObjectLog(mirror::Object* obj) { return object_logs_.GetOrCreate(obj, [&]() { return ObjectLog(&allocator_); }); } @@ -174,8 +211,10 @@ void Transaction::RecordWriteFieldBoolean(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.LogBooleanValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.LogBooleanValue(field_offset, value, is_volatile); + } } void Transaction::RecordWriteFieldByte(mirror::Object* obj, @@ -184,8 +223,10 @@ void Transaction::RecordWriteFieldByte(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.LogByteValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.LogByteValue(field_offset, value, is_volatile); + } } void Transaction::RecordWriteFieldChar(mirror::Object* obj, @@ -194,8 +235,10 @@ void Transaction::RecordWriteFieldChar(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.LogCharValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.LogCharValue(field_offset, value, is_volatile); + } } @@ -205,8 +248,10 @@ void Transaction::RecordWriteFieldShort(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.LogShortValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.LogShortValue(field_offset, value, is_volatile); + } } @@ -216,8 +261,10 @@ void Transaction::RecordWriteField32(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.Log32BitsValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.Log32BitsValue(field_offset, value, is_volatile); + } } void Transaction::RecordWriteField64(mirror::Object* obj, @@ -226,8 +273,10 @@ void Transaction::RecordWriteField64(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.Log64BitsValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.Log64BitsValue(field_offset, value, is_volatile); + } } void Transaction::RecordWriteFieldReference(mirror::Object* obj, @@ -236,8 +285,10 @@ void Transaction::RecordWriteFieldReference(mirror::Object* obj, bool is_volatile) { DCHECK(obj != nullptr); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ObjectLog& object_log = GetOrCreateObjectLog(obj); - object_log.LogReferenceValue(field_offset, value, is_volatile); + if (obj != last_allocated_object_) { + ObjectLog& object_log = GetOrCreateObjectLog(obj); + object_log.LogReferenceValue(field_offset, value, is_volatile); + } } void Transaction::RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) { @@ -245,8 +296,10 @@ void Transaction::RecordWriteArray(mirror::Array* array, size_t index, uint64_t DCHECK(array->IsArrayInstance()); DCHECK(!array->IsObjectArray()); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; - ArrayLog& array_log = array_logs_.GetOrCreate(array, [&]() { return ArrayLog(&allocator_); }); - array_log.LogValue(index, value); + if (array != last_allocated_object_) { + ArrayLog& array_log = array_logs_.GetOrCreate(array, [&]() { return ArrayLog(&allocator_); }); + array_log.LogValue(index, value); + } } void Transaction::RecordResolveString(ObjPtr<mirror::DexCache> dex_cache, @@ -355,6 +408,7 @@ void Transaction::VisitRoots(RootVisitor* visitor) { DCHECK(Locks::mutator_lock_->IsExclusiveHeld(Thread::Current())); visitor->VisitRoot(reinterpret_cast<mirror::Object**>(&root_), RootInfo(kRootUnknown)); + visitor->VisitRoot(&last_allocated_object_, RootInfo(kRootUnknown)); { // Create a separate `ArenaStack` for this thread. ArenaStack arena_stack(Runtime::Current()->GetArenaPool()); @@ -471,6 +525,9 @@ void Transaction::ObjectLog::LogValue(ObjectLog::FieldValueKind kind, MemberOffset offset, uint64_t value, bool is_volatile) { + if (is_new_object_) { + return; + } auto it = field_values_.find(offset.Uint32Value()); if (it == field_values_.end()) { ObjectLog::FieldValue field_value; @@ -679,6 +736,9 @@ Transaction::InternStringLog::InternStringLog(ObjPtr<mirror::String> s, } void Transaction::ArrayLog::LogValue(size_t index, uint64_t value) { + if (is_new_array_) { + return; + } // Add a mapping if there is none yet. array_values_.FindOrAdd(index, value); } @@ -744,7 +804,8 @@ void Transaction::ArrayLog::UndoArrayWrite(mirror::Array* array, Transaction* ScopedAssertNoNewTransactionRecords::InstallAssertion(const char* reason) { Transaction* transaction = nullptr; if (kIsDebugBuild && Runtime::Current()->IsActiveTransaction()) { - transaction = Runtime::Current()->GetTransaction(); + AotClassLinker* class_linker = down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + transaction = class_linker->GetTransaction(); if (transaction != nullptr) { CHECK(transaction->assert_no_new_records_reason_ == nullptr) << "old: " << transaction->assert_no_new_records_reason_ << " new: " << reason; @@ -756,7 +817,8 @@ Transaction* ScopedAssertNoNewTransactionRecords::InstallAssertion(const char* r void ScopedAssertNoNewTransactionRecords::RemoveAssertion(Transaction* transaction) { if (kIsDebugBuild) { - CHECK(Runtime::Current()->GetTransaction() == transaction); + AotClassLinker* class_linker = down_cast<AotClassLinker*>(Runtime::Current()->GetClassLinker()); + CHECK(class_linker->GetTransaction() == transaction); CHECK(transaction->assert_no_new_records_reason_ != nullptr); transaction->assert_no_new_records_reason_ = nullptr; } diff --git a/runtime/transaction.h b/runtime/transaction.h index 847120234d..60d1b3e3fd 100644 --- a/runtime/transaction.h +++ b/runtime/transaction.h @@ -74,6 +74,24 @@ class Transaction final { return strict_; } + // Record newly allocated object/array. + // + // There is no reason to record old values for newly allocated objects because they become + // unreachable when the transaction is rolled back, so their data does not need to be rolled back. + // + // Implementation details: We track all newly allocated objects/arrays by creating an + // `ObjectLog`/`ArrayLog` flagged as a new object/array. We also cache the last allocated + // object/array which often helps avoid the search for the flagged `ObjectLog`/`ArrayLog`. + void RecordNewObject(ObjPtr<mirror::Object> allocated_object) + REQUIRES_SHARED(Locks::mutator_lock_); + void RecordNewArray(ObjPtr<mirror::Array> allocated_object) + REQUIRES_SHARED(Locks::mutator_lock_); + + bool ObjectNeedsTransactionRecords(ObjPtr<mirror::Object> obj) + REQUIRES_SHARED(Locks::mutator_lock_); + bool ArrayNeedsTransactionRecords(ObjPtr<mirror::Array> array) + REQUIRES_SHARED(Locks::mutator_lock_); + // Record object field changes. void RecordWriteFieldBoolean(mirror::Object* obj, MemberOffset field_offset, @@ -160,8 +178,18 @@ class Transaction final { return field_values_.size(); } + void MarkAsNewObject() { + DCHECK(field_values_.empty()); + is_new_object_ = true; + } + + bool IsNewObject() const { + return is_new_object_; + } + explicit ObjectLog(ScopedArenaAllocator* allocator) - : field_values_(std::less<uint32_t>(), allocator->Adapter(kArenaAllocTransaction)) {} + : is_new_object_(false), + field_values_(std::less<uint32_t>(), allocator->Adapter(kArenaAllocTransaction)) {} ObjectLog(ObjectLog&& log) = default; private: @@ -192,6 +220,10 @@ class Transaction final { MemberOffset field_offset, const FieldValue& field_value) const REQUIRES_SHARED(Locks::mutator_lock_); + // Whether this is a new object. We do not need to keep transaction records for objects + // created inside a transaction because they become unreachable on rollback. + bool is_new_object_; + // Maps field's offset to its value. ScopedArenaSafeMap<uint32_t, FieldValue> field_values_; @@ -208,8 +240,18 @@ class Transaction final { return array_values_.size(); } + void MarkAsNewArray() { + DCHECK(array_values_.empty()); + is_new_array_ = true; + } + + bool IsNewArray() const { + return is_new_array_; + } + explicit ArrayLog(ScopedArenaAllocator* allocator) - : array_values_(std::less<size_t>(), allocator->Adapter(kArenaAllocTransaction)) {} + : is_new_array_(false), + array_values_(std::less<size_t>(), allocator->Adapter(kArenaAllocTransaction)) {} ArrayLog(ArrayLog&& log) = default; @@ -219,6 +261,10 @@ class Transaction final { size_t index, uint64_t value) const REQUIRES_SHARED(Locks::mutator_lock_); + // Whether this is a new array. We do not need to keep transaction records for arrays + // created inside a transaction because they become unreachable on rollback. + bool is_new_array_; + // Maps index to value. // TODO use JValue instead ? ScopedArenaSafeMap<size_t, uint64_t> array_values_; @@ -333,6 +379,7 @@ class Transaction final { const bool strict_; std::string abort_message_; mirror::Class* root_; + mirror::Object* last_allocated_object_; const char* assert_no_new_records_reason_; friend class ScopedAssertNoNewTransactionRecords; diff --git a/runtime/transaction_test.cc b/runtime/transaction_test.cc index e52cef46e0..17a78ad6af 100644 --- a/runtime/transaction_test.cc +++ b/runtime/transaction_test.cc @@ -19,7 +19,7 @@ #include "art_field-inl.h" #include "art_method-inl.h" #include "class_linker-inl.h" -#include "common_runtime_test.h" +#include "common_transaction_test.h" #include "common_throws.h" #include "dex/dex_file.h" #include "mirror/array-alloc-inl.h" @@ -28,7 +28,7 @@ namespace art HIDDEN { -class TransactionTest : public CommonRuntimeTest { +class TransactionTest : public CommonTransactionTest { protected: TransactionTest() { this->use_boot_image_ = true; // We need the boot image for this test. diff --git a/test/2230-profile-save-hotness/run.py b/test/2230-profile-save-hotness/run.py index 526b841b87..e2f4473b86 100644 --- a/test/2230-profile-save-hotness/run.py +++ b/test/2230-profile-save-hotness/run.py @@ -16,10 +16,11 @@ def run(ctx, args): ctx.default_run( args, + # Profiling is only done on interpreted and JITted code. Xcompiler_option=[ - "--count-hotness-in-compiled-code", "--compiler-filter=speed" + "--compiler-filter=verify" ], runtime_option=[ - "-Xps-profile-aot-code", "-Xjitsaveprofilinginfo", "-Xusejit:true" + "-Xjitsaveprofilinginfo", "-Xusejit:true" ], ) diff --git a/test/2230-profile-save-hotness/src-art/Main.java b/test/2230-profile-save-hotness/src-art/Main.java index c9132e6e7d..06e89b2ade 100644 --- a/test/2230-profile-save-hotness/src-art/Main.java +++ b/test/2230-profile-save-hotness/src-art/Main.java @@ -47,7 +47,7 @@ public class Main { new String[] {codePath}, VMRuntime.CODE_PATH_TYPE_PRIMARY_APK); - // Test that the profile saves an app method with a profiling info. + // Test that the profile saves an app method that gets JITted. $noinline$hotnessCountWithLoop(100000); ensureProfileProcessing(); Method appMethod = Main.class.getDeclaredMethod(methodName); @@ -72,7 +72,7 @@ public class Main { } } - // Checks if the profiles saver has the method as hot/warm. + // Checks if the profile saver has the method as hot/warm. public static native boolean presentInProfile(String profile, Method method); // Ensures the profile saver does its usual processing. public static native void ensureProfileProcessing(); diff --git a/test/2256-checker-vector-replacement/src/Main.java b/test/2256-checker-vector-replacement/src/Main.java index fca80fbeda..2f94094577 100644 --- a/test/2256-checker-vector-replacement/src/Main.java +++ b/test/2256-checker-vector-replacement/src/Main.java @@ -20,30 +20,34 @@ public class Main { } // Before loop optimization we only had an array get. After it, we optimized to also have - // VecLoad operations. This happens consistently only for Arm64. Arm32 vectorizes consistently - // but it also removes the ArrayGet. X86/X86_64 doesn't vectorize consistently (other - // vectorization tests also ignore x86/x86_64). + // VecLoad operations. This happens consistently only for Arm64 when using traditional + // vectorization (NEON). Arm32 vectorizes consistently but it also removes the ArrayGet, as does + // Arm64 predicated vectorization (SVE) because the scalar tail loop is eliminated. X86/X86_64 + // doesn't vectorize consistently (other vectorization tests also ignore x86/x86_64). + // TODO: Create equivalent ArrayGet-replacement regression test for SVE, when SVE supports LSE. /// CHECK-START-ARM64: void Main.$noinline$testVectorAndNonVector() loop_optimization (before) - /// CHECK: ArrayGet + /// CHECK-IF: not (hasIsaFeature("sve") and os.environ.get('ART_FORCE_TRY_PREDICATED_SIMD') == 'true') + /// CHECK-DAG: ArrayGet + /// CHECK-NOT: VecLoad + /// CHECK-FI: /// CHECK-START-ARM64: void Main.$noinline$testVectorAndNonVector() loop_optimization (after) - /// CHECK: ArrayGet - - /// CHECK-START-ARM64: void Main.$noinline$testVectorAndNonVector() loop_optimization (before) - /// CHECK-NOT: VecLoad - - /// CHECK-START-ARM64: void Main.$noinline$testVectorAndNonVector() loop_optimization (after) - /// CHECK: VecLoad + /// CHECK-IF: not (hasIsaFeature("sve") and os.environ.get('ART_FORCE_TRY_PREDICATED_SIMD') == 'true') + /// CHECK-DAG: ArrayGet + /// CHECK-DAG: VecLoad + /// CHECK-FI: // In LoadStoreElimination both ArrayGet and VecLoad have the same heap location. We will try to // replace the ArrayGet with the constant 0. The crash happens when we want to do the same with // the vector operation, changing the vector operation to a scalar. /// CHECK-START-ARM64: void Main.$noinline$testVectorAndNonVector() load_store_elimination (before) - /// CHECK-DAG: VecLoad outer_loop:<<VecBlock:B\d+>> - /// CHECK-DAG: ArrayGet outer_loop:<<ScalarBlock:B\d+>> - /// CHECK-EVAL: "<<VecBlock>>" == "<<ScalarBlock>>" + /// CHECK-IF: not (hasIsaFeature("sve") and os.environ.get('ART_FORCE_TRY_PREDICATED_SIMD') == 'true') + /// CHECK-DAG: VecLoad outer_loop:<<VecBlock:B\d+>> + /// CHECK-DAG: ArrayGet outer_loop:<<ScalarBlock:B\d+>> + /// CHECK-EVAL: "<<VecBlock>>" == "<<ScalarBlock>>" + /// CHECK-FI: private static void $noinline$testVectorAndNonVector() { int[] result = new int[2]; diff --git a/test/623-checker-loop-regressions/src/Main.java b/test/623-checker-loop-regressions/src/Main.java index 134e90c6c5..5e9b3d7e15 100644 --- a/test/623-checker-loop-regressions/src/Main.java +++ b/test/623-checker-loop-regressions/src/Main.java @@ -977,6 +977,39 @@ public class Main { return s0 + s1; } + // Regression test for the case, where a loop is vectorized in predicated mode, and there is + // a disambiguation scalar loop added. Make sure that the set, which records instructions + // inserted outside of new loops, is not reset until the full vectorization process has + // happened. + // + // Based on void android.util.Spline$MonotoneCubicSpline.<init>(float[], float[]). + // + /// CHECK-START-ARM64: void Main.$noinline$testExternalSetForLoopWithDisambiguation(int[], int[]) loop_optimization (after) + /// CHECK-IF: hasIsaFeature("sve") and os.environ.get('ART_FORCE_TRY_PREDICATED_SIMD') == 'true' + // + /// CHECK-DAG: <<Pred:j\d+>> VecPredSetAll loop:none + /// CHECK-DAG: VecReplicateScalar [{{i\d+}},<<Pred>>] loop:none + // + /// CHECK-ELSE: + // + /// CHECK-DAG: VecReplicateScalar loop:none + // + /// CHECK-FI: + // + // Vector loop. + /// CHECK-DAG: Phi loop:<<VectorLoop:B\d+>> outer_loop:none + /// CHECK-DAG: VecLoad loop:<<VectorLoop>> outer_loop:none + // + // Backup scalar loop. + /// CHECK-DAG: Phi loop:<<ScalarLoop:B\d+>> outer_loop:none + /// CHECK-DAG: ArrayGet loop:<<ScalarLoop>> outer_loop:none + public static void $noinline$testExternalSetForLoopWithDisambiguation(int[] d, int[] m) { + m[0] = d[0]; + for (int i = 1; i < m.length; i++) { + m[i] = (d[i - 1] + d[i]) * 53; + } + } + public static final int ARRAY_SIZE = 512; private static byte[] createAndInitByteArray(int x) { @@ -1253,6 +1286,17 @@ public class Main { byte[] b_b = createAndInitByteArray(2); expectEquals(1278, testSADAndDotProdCombined1(b_a, b_b)); } + { + int[] i_a = createAndInitIntArray(1); + int[] i_b = createAndInitIntArray(2); + $noinline$testExternalSetForLoopWithDisambiguation(i_a, i_b); + + int sum = 0; + for (int i = 0; i < i_b.length; i++) { + sum += i_b[i]; + } + expectEquals(-13839413, sum); + } System.out.println("passed"); } diff --git a/test/667-jit-jni-stub/jit_jni_stub_test.cc b/test/667-jit-jni-stub/jit_jni_stub_test.cc index 2b8e14c03d..52f467e38b 100644 --- a/test/667-jit-jni-stub/jit_jni_stub_test.cc +++ b/test/667-jit-jni-stub/jit_jni_stub_test.cc @@ -39,9 +39,15 @@ extern "C" JNIEXPORT void Java_Main_jitGc(JNIEnv*, jclass) { CHECK(Runtime::Current()->GetJit() != nullptr); jit::JitCodeCache* cache = Runtime::Current()->GetJit()->GetCodeCache(); - ScopedObjectAccess soa(Thread::Current()); - cache->InvalidateAllCompiledCode(); - cache->GarbageCollectCache(Thread::Current()); + Thread* self = Thread::Current(); + { + ScopedObjectAccess soa(self); + cache->InvalidateAllCompiledCode(); + } + cache->DoCollection(self); + // Run a second time in case the first run was a no-op due to a concurrent JIT + // GC from the JIT thread. + cache->DoCollection(self); } extern "C" JNIEXPORT diff --git a/test/667-jit-jni-stub/src/Main.java b/test/667-jit-jni-stub/src/Main.java index c0829b5100..05039670d1 100644 --- a/test/667-jit-jni-stub/src/Main.java +++ b/test/667-jit-jni-stub/src/Main.java @@ -67,9 +67,6 @@ public class Main { assertFalse(hasJitCompiledEntrypoint(Main.class, "callThrough")); assertFalse(hasJitCompiledCode(Main.class, "callThrough")); callThrough(Main.class, "testMixedFramesOnStackStage2"); - // We have just returned through the JIT-compiled JNI stub, so it must still - // be compiled (though not necessarily with the entrypoint pointing to it). - assertTrue(hasJitCompiledCode(Main.class, "callThrough")); // Though the callThrough() is on the stack, that frame is using the GenericJNI // and does not prevent the collection of the JNI stub. testStubCanBeCollected(); @@ -94,10 +91,6 @@ public class Main { } public static void testStubCanBeCollected() { - assertTrue(hasJitCompiledCode(Main.class, "callThrough")); - doJitGcsUntilFullJitGcIsScheduled(); - assertFalse(hasJitCompiledEntrypoint(Main.class, "callThrough")); - assertTrue(hasJitCompiledCode(Main.class, "callThrough")); jitGc(); // JIT GC without callThrough() on the stack should collect the callThrough() stub. assertFalse(hasJitCompiledEntrypoint(Main.class, "callThrough")); assertFalse(hasJitCompiledCode(Main.class, "callThrough")); diff --git a/test/855-native/expected-stderr.txt b/test/855-native/expected-stderr.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/test/855-native/expected-stderr.txt diff --git a/test/855-native/expected-stdout.txt b/test/855-native/expected-stdout.txt new file mode 100644 index 0000000000..6a5618ebc6 --- /dev/null +++ b/test/855-native/expected-stdout.txt @@ -0,0 +1 @@ +JNI_OnLoad called diff --git a/test/855-native/info.txt b/test/855-native/info.txt new file mode 100644 index 0000000000..b1bf24e925 --- /dev/null +++ b/test/855-native/info.txt @@ -0,0 +1,2 @@ +Regression test for b/342077744: handle the case we may running generic JNI for +a method but its entrypoint is a shared boot image JNI stub. diff --git a/test/855-native/run.py b/test/855-native/run.py new file mode 100644 index 0000000000..b02a3f962c --- /dev/null +++ b/test/855-native/run.py @@ -0,0 +1,20 @@ +#!/bin/bash +# +# Copyright 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Pass no prebuild to not have an oat file for the test. If there is an oat +# file, the runtime doesn't hit the crash. +def run(ctx, args): + ctx.default_run(args, prebuild=False) diff --git a/test/855-native/src-art/Main.java b/test/855-native/src-art/Main.java new file mode 100644 index 0000000000..2ac83e5c36 --- /dev/null +++ b/test/855-native/src-art/Main.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.IOException; + +import dalvik.system.VMDebug; + +public class Main { + + private static final String TEMP_FILE_NAME_PREFIX = "test"; + private static final String TEMP_FILE_NAME_SUFFIX = ".trace"; + + private static File createTempFile() throws Exception { + try { + return File.createTempFile(TEMP_FILE_NAME_PREFIX, TEMP_FILE_NAME_SUFFIX); + } catch (IOException e) { + System.setProperty("java.io.tmpdir", "/data/local/tmp"); + try { + return File.createTempFile(TEMP_FILE_NAME_PREFIX, TEMP_FILE_NAME_SUFFIX); + } catch (IOException e2) { + System.setProperty("java.io.tmpdir", "/sdcard"); + return File.createTempFile(TEMP_FILE_NAME_PREFIX, TEMP_FILE_NAME_SUFFIX); + } + } + } + + public static void main(String[] args) throws Exception { + System.loadLibrary(args[0]); + + // In case we already run in tracing mode, disable it. + if (VMDebug.getMethodTracingMode() != 0) { + VMDebug.stopMethodTracing(); + } + + File tempFile = createTempFile(); + + // Start method tracing so that native methods get the generic JNI stub. + VMDebug.startMethodTracing(tempFile.getPath(), 0, 0, false, 0); + + // We need the caller of `throwsException` to be nterp or compiled, because the clinit check is + // executed within the generic JNI stub. The switch interpreter does a clinit check before the + // invoke, and that doesn't trigger the bug. + Main.ensureJitCompiled(Runner.class, "run"); + + // We want the `Test` class to be loaded after `startMethodTracing`. So we call it through + // `Runner` to avoid the verification of the `Main` class preload `Test`. + Runner.run(); + } + + public static native void ensureJitCompiled(Class<?> cls, String method); +} + +class Runner { + public static void run() { + try { + // Will call through the generic JNI stub and when returning from the native code will need + // to walk the stack to throw the exception. We used to crash when walking the stack because + // we did not expect to have a generic JNI PC with an entrypoint from a shared boot image + // JNI stub. + Test.throwsException(); + } catch (Test e) { + if (!Test.ranInitializer) { + throw new Error("Expected Test.ranInitializer to be true"); + } + return; + } + throw new Error("Expected exception"); + } +} + +class Test extends Exception { + static { + ranInitializer = true; + // Will update the entrypoint of `Test.throwsException` to point to a JNI stub from the boot + // image. + VMDebug.stopMethodTracing(); + } + + static boolean ranInitializer; + static native void throwsException() throws Test; +} diff --git a/test/855-native/throws_exception.cc b/test/855-native/throws_exception.cc new file mode 100644 index 0000000000..6cf5c5a3dc --- /dev/null +++ b/test/855-native/throws_exception.cc @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "jni.h" + +namespace { + +extern "C" JNIEXPORT void JNICALL Java_Test_throwsException( + JNIEnv* env, jclass exClass) { + env->ThrowNew(exClass, "In native"); +} + +} // namespace diff --git a/test/856-clone/expected-stderr.txt b/test/856-clone/expected-stderr.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/test/856-clone/expected-stderr.txt diff --git a/test/856-clone/expected-stdout.txt b/test/856-clone/expected-stdout.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/test/856-clone/expected-stdout.txt diff --git a/test/856-clone/info.txt b/test/856-clone/info.txt new file mode 100644 index 0000000000..c94aab511f --- /dev/null +++ b/test/856-clone/info.txt @@ -0,0 +1,2 @@ +Regression test for calling `Object.clone` from within an default interface +method. diff --git a/test/856-clone/src/Main.java b/test/856-clone/src/Main.java new file mode 100644 index 0000000000..278cfd9542 --- /dev/null +++ b/test/856-clone/src/Main.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface I { + default String[] myClone(String[] strings) { + return strings.clone(); + } +} + +class A implements I {} + +public class Main { + public static void main(String[] args) { + new A().myClone(args); + } +} diff --git a/test/989-method-trace-throw/expected-stdout.txt b/test/989-method-trace-throw/expected-stdout.txt index 0911bc35e8..6bd7357699 100644 --- a/test/989-method-trace-throw/expected-stdout.txt +++ b/test/989-method-trace-throw/expected-stdout.txt @@ -10,6 +10,11 @@ Received expected error for test[class art.Test989$NormalTracer, class art.Test9 Normal: Entering public static native void art.Test989.throwANative() Normal: Leaving public static native void art.Test989.throwANative() returned <exception> Received expected error for test[class art.Test989$NormalTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorA: Throwing Error A +Normal: Entering public static void art.Test989.throwNativeException() +Normal: Entering public static native void art.Test989.doThrowNative() +Normal: Leaving public static native void art.Test989.doThrowNative() returned <exception> +Normal: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$NormalTracer, class art.Test989$throwNativeExceptionClass] - java.lang.Error: Error Normal: Entering public static java.lang.Object art.Test989.returnValue() Normal: Leaving public static java.lang.Object art.Test989.returnValue() returned TestObject(0) returnValue returned: TestObject(0) @@ -57,6 +62,9 @@ Received expected error for test[class art.Test989$ThrowEnterTracer, class art.T ThrowEnter: Entering public static native void art.Test989.throwANative() ThrowEnter: Leaving public static native void art.Test989.throwANative() returned <exception> Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorB: Throwing error while entering public static native void art.Test989.throwANative() +ThrowEnter: Entering public static void art.Test989.throwNativeException() +ThrowEnter: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$throwNativeExceptionClass] - art.Test989$ErrorB: Throwing error while entering public static void art.Test989.throwNativeException() ThrowEnter: Entering public static java.lang.Object art.Test989.returnValue() ThrowEnter: Leaving public static java.lang.Object art.Test989.returnValue() returned <exception> Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnValueClass] - art.Test989$ErrorB: Throwing error while entering public static java.lang.Object art.Test989.returnValue() @@ -96,6 +104,11 @@ Received expected error for test[class art.Test989$ThrowExitTracer, class art.Te ThrowExit: Entering public static native void art.Test989.throwANative() ThrowExit: Leaving public static native void art.Test989.throwANative() returned <exception> Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorB: Throwing error while exit public static native void art.Test989.throwANative() returned <exception> +ThrowExit: Entering public static void art.Test989.throwNativeException() +ThrowExit: Entering public static native void art.Test989.doThrowNative() +ThrowExit: Leaving public static native void art.Test989.doThrowNative() returned <exception> +ThrowExit: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$throwNativeExceptionClass] - art.Test989$ErrorB: Throwing error while exit public static void art.Test989.throwNativeException() returned <exception> ThrowExit: Entering public static java.lang.Object art.Test989.returnValue() ThrowExit: Leaving public static java.lang.Object art.Test989.returnValue() returned TestObject(7) Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnValueClass] - art.Test989$ErrorB: Throwing error while exit public static java.lang.Object art.Test989.returnValue() returned TestObject(7) @@ -137,6 +150,9 @@ Received expected error for test[class art.Test989$ThrowBothTracer, class art.Te ThrowBoth: Entering public static native void art.Test989.throwANative() ThrowBoth: Leaving public static native void art.Test989.throwANative() returned <exception> Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorC: Throwing error while exit public static native void art.Test989.throwANative() returned <exception> +ThrowBoth: Entering public static void art.Test989.throwNativeException() +ThrowBoth: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$throwNativeExceptionClass] - art.Test989$ErrorC: Throwing error while exit public static void art.Test989.throwNativeException() returned <exception> ThrowBoth: Entering public static java.lang.Object art.Test989.returnValue() ThrowBoth: Leaving public static java.lang.Object art.Test989.returnValue() returned <exception> Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnValueClass] - art.Test989$ErrorC: Throwing error while exit public static java.lang.Object art.Test989.returnValue() returned <exception> @@ -168,6 +184,7 @@ Received no exception as expected for test[class art.Test989$ForceGCTracer, clas Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$doNothingNativeClass]. Received expected error for test[class art.Test989$ForceGCTracer, class art.Test989$throwAClass] - art.Test989$ErrorA: Throwing Error A Received expected error for test[class art.Test989$ForceGCTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorA: Throwing Error A +Received expected error for test[class art.Test989$ForceGCTracer, class art.Test989$throwNativeExceptionClass] - java.lang.Error: Error returnValue returned: TestObject(14) Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnValueClass]. returnValueNative returned: TestObject(15) @@ -185,4 +202,209 @@ returnDouble returned: 3.14159628 Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnDoubleClass]. returnDoubleNative returned: 3.14159628 Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnDoubleNativeClass]. +Finished - without non-standard exits! +Normal: Entering public static void art.Test989.doNothing() +Normal: Leaving public static void art.Test989.doNothing() returned null +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$doNothingClass]. +Normal: Entering public static native void art.Test989.doNothingNative() +Normal: Leaving public static native void art.Test989.doNothingNative() returned null +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$doNothingNativeClass]. +Normal: Entering public static void art.Test989.throwA() +Normal: Leaving public static void art.Test989.throwA() returned <exception> +Received expected error for test[class art.Test989$NormalTracer, class art.Test989$throwAClass] - art.Test989$ErrorA: Throwing Error A +Normal: Entering public static native void art.Test989.throwANative() +Normal: Leaving public static native void art.Test989.throwANative() returned <exception> +Received expected error for test[class art.Test989$NormalTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorA: Throwing Error A +Normal: Entering public static void art.Test989.throwNativeException() +Normal: Entering public static native void art.Test989.doThrowNative() +Normal: Leaving public static native void art.Test989.doThrowNative() returned <exception> +Normal: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$NormalTracer, class art.Test989$throwNativeExceptionClass] - java.lang.Error: Error +Normal: Entering public static java.lang.Object art.Test989.returnValue() +Normal: Leaving public static java.lang.Object art.Test989.returnValue() returned TestObject(19) +returnValue returned: TestObject(19) +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$returnValueClass]. +Normal: Entering public static native java.lang.Object art.Test989.returnValueNative() +Normal: Leaving public static native java.lang.Object art.Test989.returnValueNative() returned TestObject(20) +returnValueNative returned: TestObject(20) +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$returnValueNativeClass]. +Normal: Entering public static void art.Test989.acceptValue(java.lang.Object) +Recieved TestObject(21) +Normal: Leaving public static void art.Test989.acceptValue(java.lang.Object) returned null +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$acceptValueClass]. +Normal: Entering public static native void art.Test989.acceptValueNative(java.lang.Object) +Recieved TestObject(22) +Normal: Leaving public static native void art.Test989.acceptValueNative(java.lang.Object) returned null +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$acceptValueNativeClass]. +Normal: Entering public static void art.Test989.tryCatchExit() +Normal: Leaving public static void art.Test989.tryCatchExit() returned null +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$tryCatchExitClass]. +Normal: Entering public static float art.Test989.returnFloat() +Normal: Leaving public static float art.Test989.returnFloat() returned 1.618 +returnFloat returned: 1.618 +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$returnFloatClass]. +Normal: Entering public static native float art.Test989.returnFloatNative() +Normal: Leaving public static native float art.Test989.returnFloatNative() returned 1.618 +returnFloatNative returned: 1.618 +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$returnFloatNativeClass]. +Normal: Entering public static double art.Test989.returnDouble() +Normal: Leaving public static double art.Test989.returnDouble() returned 3.14159628 +returnDouble returned: 3.14159628 +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$returnDoubleClass]. +Normal: Entering public static native double art.Test989.returnDoubleNative() +Normal: Leaving public static native double art.Test989.returnDoubleNative() returned 3.14159628 +returnDoubleNative returned: 3.14159628 +Received no exception as expected for test[class art.Test989$NormalTracer, class art.Test989$returnDoubleNativeClass]. +ThrowEnter: Entering public static void art.Test989.doNothing() +ThrowEnter: Leaving public static void art.Test989.doNothing() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$doNothingClass] - art.Test989$ErrorB: Throwing error while entering public static void art.Test989.doNothing() +ThrowEnter: Entering public static native void art.Test989.doNothingNative() +ThrowEnter: Leaving public static native void art.Test989.doNothingNative() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$doNothingNativeClass] - art.Test989$ErrorB: Throwing error while entering public static native void art.Test989.doNothingNative() +ThrowEnter: Entering public static void art.Test989.throwA() +ThrowEnter: Leaving public static void art.Test989.throwA() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$throwAClass] - art.Test989$ErrorB: Throwing error while entering public static void art.Test989.throwA() +ThrowEnter: Entering public static native void art.Test989.throwANative() +ThrowEnter: Leaving public static native void art.Test989.throwANative() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorB: Throwing error while entering public static native void art.Test989.throwANative() +ThrowEnter: Entering public static void art.Test989.throwNativeException() +ThrowEnter: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$throwNativeExceptionClass] - art.Test989$ErrorB: Throwing error while entering public static void art.Test989.throwNativeException() +ThrowEnter: Entering public static java.lang.Object art.Test989.returnValue() +ThrowEnter: Leaving public static java.lang.Object art.Test989.returnValue() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnValueClass] - art.Test989$ErrorB: Throwing error while entering public static java.lang.Object art.Test989.returnValue() +ThrowEnter: Entering public static native java.lang.Object art.Test989.returnValueNative() +ThrowEnter: Leaving public static native java.lang.Object art.Test989.returnValueNative() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnValueNativeClass] - art.Test989$ErrorB: Throwing error while entering public static native java.lang.Object art.Test989.returnValueNative() +ThrowEnter: Entering public static void art.Test989.acceptValue(java.lang.Object) +ThrowEnter: Leaving public static void art.Test989.acceptValue(java.lang.Object) returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$acceptValueClass] - art.Test989$ErrorB: Throwing error while entering public static void art.Test989.acceptValue(java.lang.Object) +ThrowEnter: Entering public static native void art.Test989.acceptValueNative(java.lang.Object) +ThrowEnter: Leaving public static native void art.Test989.acceptValueNative(java.lang.Object) returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$acceptValueNativeClass] - art.Test989$ErrorB: Throwing error while entering public static native void art.Test989.acceptValueNative(java.lang.Object) +ThrowEnter: Entering public static void art.Test989.tryCatchExit() +ThrowEnter: Leaving public static void art.Test989.tryCatchExit() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$tryCatchExitClass] - art.Test989$ErrorB: Throwing error while entering public static void art.Test989.tryCatchExit() +ThrowEnter: Entering public static float art.Test989.returnFloat() +ThrowEnter: Leaving public static float art.Test989.returnFloat() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnFloatClass] - art.Test989$ErrorB: Throwing error while entering public static float art.Test989.returnFloat() +ThrowEnter: Entering public static native float art.Test989.returnFloatNative() +ThrowEnter: Leaving public static native float art.Test989.returnFloatNative() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnFloatNativeClass] - art.Test989$ErrorB: Throwing error while entering public static native float art.Test989.returnFloatNative() +ThrowEnter: Entering public static double art.Test989.returnDouble() +ThrowEnter: Leaving public static double art.Test989.returnDouble() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnDoubleClass] - art.Test989$ErrorB: Throwing error while entering public static double art.Test989.returnDouble() +ThrowEnter: Entering public static native double art.Test989.returnDoubleNative() +ThrowEnter: Leaving public static native double art.Test989.returnDoubleNative() returned <exception> +Received expected error for test[class art.Test989$ThrowEnterTracer, class art.Test989$returnDoubleNativeClass] - art.Test989$ErrorB: Throwing error while entering public static native double art.Test989.returnDoubleNative() +ThrowExit: Entering public static void art.Test989.doNothing() +ThrowExit: Leaving public static void art.Test989.doNothing() returned null +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$doNothingClass] - art.Test989$ErrorB: Throwing error while exit public static void art.Test989.doNothing() returned null +ThrowExit: Entering public static native void art.Test989.doNothingNative() +ThrowExit: Leaving public static native void art.Test989.doNothingNative() returned null +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$doNothingNativeClass] - art.Test989$ErrorB: Throwing error while exit public static native void art.Test989.doNothingNative() returned null +ThrowExit: Entering public static void art.Test989.throwA() +ThrowExit: Leaving public static void art.Test989.throwA() returned <exception> +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$throwAClass] - art.Test989$ErrorB: Throwing error while exit public static void art.Test989.throwA() returned <exception> +ThrowExit: Entering public static native void art.Test989.throwANative() +ThrowExit: Leaving public static native void art.Test989.throwANative() returned <exception> +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorB: Throwing error while exit public static native void art.Test989.throwANative() returned <exception> +ThrowExit: Entering public static void art.Test989.throwNativeException() +ThrowExit: Entering public static native void art.Test989.doThrowNative() +ThrowExit: Leaving public static native void art.Test989.doThrowNative() returned <exception> +ThrowExit: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$throwNativeExceptionClass] - art.Test989$ErrorB: Throwing error while exit public static void art.Test989.throwNativeException() returned <exception> +ThrowExit: Entering public static java.lang.Object art.Test989.returnValue() +ThrowExit: Leaving public static java.lang.Object art.Test989.returnValue() returned TestObject(26) +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnValueClass] - art.Test989$ErrorB: Throwing error while exit public static java.lang.Object art.Test989.returnValue() returned TestObject(26) +ThrowExit: Entering public static native java.lang.Object art.Test989.returnValueNative() +ThrowExit: Leaving public static native java.lang.Object art.Test989.returnValueNative() returned TestObject(27) +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnValueNativeClass] - art.Test989$ErrorB: Throwing error while exit public static native java.lang.Object art.Test989.returnValueNative() returned TestObject(27) +ThrowExit: Entering public static void art.Test989.acceptValue(java.lang.Object) +Recieved TestObject(28) +ThrowExit: Leaving public static void art.Test989.acceptValue(java.lang.Object) returned null +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$acceptValueClass] - art.Test989$ErrorB: Throwing error while exit public static void art.Test989.acceptValue(java.lang.Object) returned null +ThrowExit: Entering public static native void art.Test989.acceptValueNative(java.lang.Object) +Recieved TestObject(29) +ThrowExit: Leaving public static native void art.Test989.acceptValueNative(java.lang.Object) returned null +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$acceptValueNativeClass] - art.Test989$ErrorB: Throwing error while exit public static native void art.Test989.acceptValueNative(java.lang.Object) returned null +ThrowExit: Entering public static void art.Test989.tryCatchExit() +ThrowExit: Leaving public static void art.Test989.tryCatchExit() returned null +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$tryCatchExitClass] - art.Test989$ErrorB: Throwing error while exit public static void art.Test989.tryCatchExit() returned null +ThrowExit: Entering public static float art.Test989.returnFloat() +ThrowExit: Leaving public static float art.Test989.returnFloat() returned 1.618 +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnFloatClass] - art.Test989$ErrorB: Throwing error while exit public static float art.Test989.returnFloat() returned 1.618 +ThrowExit: Entering public static native float art.Test989.returnFloatNative() +ThrowExit: Leaving public static native float art.Test989.returnFloatNative() returned 1.618 +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnFloatNativeClass] - art.Test989$ErrorB: Throwing error while exit public static native float art.Test989.returnFloatNative() returned 1.618 +ThrowExit: Entering public static double art.Test989.returnDouble() +ThrowExit: Leaving public static double art.Test989.returnDouble() returned 3.14159628 +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnDoubleClass] - art.Test989$ErrorB: Throwing error while exit public static double art.Test989.returnDouble() returned 3.14159628 +ThrowExit: Entering public static native double art.Test989.returnDoubleNative() +ThrowExit: Leaving public static native double art.Test989.returnDoubleNative() returned 3.14159628 +Received expected error for test[class art.Test989$ThrowExitTracer, class art.Test989$returnDoubleNativeClass] - art.Test989$ErrorB: Throwing error while exit public static native double art.Test989.returnDoubleNative() returned 3.14159628 +ThrowBoth: Entering public static void art.Test989.doNothing() +ThrowBoth: Leaving public static void art.Test989.doNothing() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$doNothingClass] - art.Test989$ErrorC: Throwing error while exit public static void art.Test989.doNothing() returned <exception> +ThrowBoth: Entering public static native void art.Test989.doNothingNative() +ThrowBoth: Leaving public static native void art.Test989.doNothingNative() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$doNothingNativeClass] - art.Test989$ErrorC: Throwing error while exit public static native void art.Test989.doNothingNative() returned <exception> +ThrowBoth: Entering public static void art.Test989.throwA() +ThrowBoth: Leaving public static void art.Test989.throwA() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$throwAClass] - art.Test989$ErrorC: Throwing error while exit public static void art.Test989.throwA() returned <exception> +ThrowBoth: Entering public static native void art.Test989.throwANative() +ThrowBoth: Leaving public static native void art.Test989.throwANative() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorC: Throwing error while exit public static native void art.Test989.throwANative() returned <exception> +ThrowBoth: Entering public static void art.Test989.throwNativeException() +ThrowBoth: Leaving public static void art.Test989.throwNativeException() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$throwNativeExceptionClass] - art.Test989$ErrorC: Throwing error while exit public static void art.Test989.throwNativeException() returned <exception> +ThrowBoth: Entering public static java.lang.Object art.Test989.returnValue() +ThrowBoth: Leaving public static java.lang.Object art.Test989.returnValue() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnValueClass] - art.Test989$ErrorC: Throwing error while exit public static java.lang.Object art.Test989.returnValue() returned <exception> +ThrowBoth: Entering public static native java.lang.Object art.Test989.returnValueNative() +ThrowBoth: Leaving public static native java.lang.Object art.Test989.returnValueNative() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnValueNativeClass] - art.Test989$ErrorC: Throwing error while exit public static native java.lang.Object art.Test989.returnValueNative() returned <exception> +ThrowBoth: Entering public static void art.Test989.acceptValue(java.lang.Object) +ThrowBoth: Leaving public static void art.Test989.acceptValue(java.lang.Object) returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$acceptValueClass] - art.Test989$ErrorC: Throwing error while exit public static void art.Test989.acceptValue(java.lang.Object) returned <exception> +ThrowBoth: Entering public static native void art.Test989.acceptValueNative(java.lang.Object) +ThrowBoth: Leaving public static native void art.Test989.acceptValueNative(java.lang.Object) returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$acceptValueNativeClass] - art.Test989$ErrorC: Throwing error while exit public static native void art.Test989.acceptValueNative(java.lang.Object) returned <exception> +ThrowBoth: Entering public static void art.Test989.tryCatchExit() +ThrowBoth: Leaving public static void art.Test989.tryCatchExit() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$tryCatchExitClass] - art.Test989$ErrorC: Throwing error while exit public static void art.Test989.tryCatchExit() returned <exception> +ThrowBoth: Entering public static float art.Test989.returnFloat() +ThrowBoth: Leaving public static float art.Test989.returnFloat() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnFloatClass] - art.Test989$ErrorC: Throwing error while exit public static float art.Test989.returnFloat() returned <exception> +ThrowBoth: Entering public static native float art.Test989.returnFloatNative() +ThrowBoth: Leaving public static native float art.Test989.returnFloatNative() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnFloatNativeClass] - art.Test989$ErrorC: Throwing error while exit public static native float art.Test989.returnFloatNative() returned <exception> +ThrowBoth: Entering public static double art.Test989.returnDouble() +ThrowBoth: Leaving public static double art.Test989.returnDouble() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnDoubleClass] - art.Test989$ErrorC: Throwing error while exit public static double art.Test989.returnDouble() returned <exception> +ThrowBoth: Entering public static native double art.Test989.returnDoubleNative() +ThrowBoth: Leaving public static native double art.Test989.returnDoubleNative() returned <exception> +Received expected error for test[class art.Test989$ThrowBothTracer, class art.Test989$returnDoubleNativeClass] - art.Test989$ErrorC: Throwing error while exit public static native double art.Test989.returnDoubleNative() returned <exception> +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$doNothingClass]. +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$doNothingNativeClass]. +Received expected error for test[class art.Test989$ForceGCTracer, class art.Test989$throwAClass] - art.Test989$ErrorA: Throwing Error A +Received expected error for test[class art.Test989$ForceGCTracer, class art.Test989$throwANativeClass] - art.Test989$ErrorA: Throwing Error A +Received expected error for test[class art.Test989$ForceGCTracer, class art.Test989$throwNativeExceptionClass] - java.lang.Error: Error +returnValue returned: TestObject(33) +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnValueClass]. +returnValueNative returned: TestObject(34) +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnValueNativeClass]. +Recieved TestObject(35) +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$acceptValueClass]. +Recieved TestObject(36) +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$acceptValueNativeClass]. +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$tryCatchExitClass]. +returnFloat returned: 1.618 +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnFloatClass]. +returnFloatNative returned: 1.618 +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnFloatNativeClass]. +returnDouble returned: 3.14159628 +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnDoubleClass]. +returnDoubleNative returned: 3.14159628 +Received no exception as expected for test[class art.Test989$ForceGCTracer, class art.Test989$returnDoubleNativeClass]. Finished! diff --git a/test/989-method-trace-throw/method_trace.cc b/test/989-method-trace-throw/method_trace.cc index edfff907a0..1f62aa7607 100644 --- a/test/989-method-trace-throw/method_trace.cc +++ b/test/989-method-trace-throw/method_trace.cc @@ -63,6 +63,12 @@ extern "C" JNIEXPORT void JNICALL Java_art_Test989_throwANative(JNIEnv* env, env->CallStaticVoidMethod(klass, targetMethod); } +extern "C" JNIEXPORT void JNICALL Java_art_Test989_doThrowNative(JNIEnv* env, + [[maybe_unused]] jclass klass) { + jclass exception_cls = env->FindClass("java/lang/Error"); + env->ThrowNew(exception_cls, "Error"); +} + extern "C" JNIEXPORT void JNICALL Java_art_Test989_acceptValueNative(JNIEnv* env, jclass klass, jobject arg) { diff --git a/test/989-method-trace-throw/src/art/Test989.java b/test/989-method-trace-throw/src/art/Test989.java index f18d5ef3d6..fec09e0934 100644 --- a/test/989-method-trace-throw/src/art/Test989.java +++ b/test/989-method-trace-throw/src/art/Test989.java @@ -37,6 +37,7 @@ public class Test989 { testMethods.add(Test989.class.getDeclaredMethod("doNothingNative")); testMethods.add(Test989.class.getDeclaredMethod("throwA")); testMethods.add(Test989.class.getDeclaredMethod("throwANative")); + testMethods.add(Test989.class.getDeclaredMethod("throwNativeException")); testMethods.add(Test989.class.getDeclaredMethod("returnFloat")); testMethods.add(Test989.class.getDeclaredMethod("returnFloatNative")); testMethods.add(Test989.class.getDeclaredMethod("returnDouble")); @@ -46,6 +47,7 @@ public class Test989 { testMethods.add(Test989.class.getDeclaredMethod("acceptValue", Object.class)); testMethods.add(Test989.class.getDeclaredMethod("acceptValueNative", Object.class)); testMethods.add(Test989.class.getDeclaredMethod("tryCatchExit")); + testMethods.add(Test989.class.getDeclaredMethod("doThrowNative")); } catch (Exception e) { throw new Error("Bad static!", e); } @@ -244,35 +246,50 @@ public class Test989 { } public static void run() throws Exception { - MyRunnable[] testCases = new MyRunnable[] { - new doNothingClass(), - new doNothingNativeClass(), - new throwAClass(), - new throwANativeClass(), - new returnValueClass(), - new returnValueNativeClass(), - new acceptValueClass(), - new acceptValueNativeClass(), - new tryCatchExitClass(), - new returnFloatClass(), - new returnFloatNativeClass(), - new returnDoubleClass(), - new returnDoubleNativeClass(), - }; - MethodTracer[] tracers = new MethodTracer[] { - new NormalTracer(), - new ThrowEnterTracer(), - new ThrowExitTracer(), - new ThrowBothTracer(), - new ForceGCTracer(), - }; - - setupTracing(); - for (MethodTracer t : tracers) { - for (MyRunnable r : testCases) { - doTest(t, r); + MyRunnable[] testCases = new MyRunnable[] { + new doNothingClass(), + new doNothingNativeClass(), + new throwAClass(), + new throwANativeClass(), + new throwNativeExceptionClass(), + new returnValueClass(), + new returnValueNativeClass(), + new acceptValueClass(), + new acceptValueNativeClass(), + new tryCatchExitClass(), + new returnFloatClass(), + new returnFloatNativeClass(), + new returnDoubleClass(), + new returnDoubleNativeClass(), + }; + MethodTracer[] tracers = new MethodTracer[] { + new NormalTracer(), + new ThrowEnterTracer(), + new ThrowExitTracer(), + new ThrowBothTracer(), + new ForceGCTracer(), + }; + + setupTracing(); + for (MethodTracer t : tracers) { + for (MyRunnable r : testCases) { + doTest(t, r); + } + } + + maybeDisableTracing(); + System.out.println("Finished - without non-standard exits!"); + Trace.disableTracing(Thread.currentThread()); + + // Enabling frame pop events force a different path and deoptimize more often. So redo the + // tests by enabling frame pop events. + Trace.enableFramePopEvents(); + setupTracing(); + for (MethodTracer t : tracers) { + for (MyRunnable r : testCases) { + doTest(t, r); + } } - } maybeDisableTracing(); System.out.println("Finished!"); @@ -299,6 +316,17 @@ public class Test989 { } } + private static final class throwNativeExceptionClass implements MyRunnable { + public void run() { + throwNativeException(); + } + + @Override + public Class<?> expectedThrow() { + return Error.class; + } + } + private static final class tryCatchExitClass implements MyRunnable { public void run() { tryCatchExit(); @@ -419,6 +447,10 @@ public class Test989 { throw new ErrorA("Throwing Error A"); } + public static void throwNativeException() { + doThrowNative(); + } + static final class TestObject { private int idx; public TestObject(int v) { @@ -464,4 +496,5 @@ public class Test989 { public static native void throwANative(); public static native float returnFloatNative(); public static native double returnDoubleNative(); + public static native void doThrowNative(); } diff --git a/test/Android.bp b/test/Android.bp index 611f7ec9b0..988dd3e5d7 100644 --- a/test/Android.bp +++ b/test/Android.bp @@ -260,18 +260,11 @@ art_cc_defaults { header_libs: [ "libnativeloader-headers", - ], - shared_libs: [ - // `libart(d)` (`art/runtime/jni/java_vm_ext.cc`) and `libnativehelper` - // (`libnativehelper/JniInvocation.c`) define symbols with the same name - // (e.g. `JNI_GetDefaultJavaVMInitArgs`). - // `JavaVmExtTest#*` tests require `libart(d)` implementation of these symbols. - // At the moment `libart(d)` is linked statically (through `libart(d)-gtest`) - // and these symbols are correctly resolved to `libart(d)`. - // If `libnativehelper` and `libart(d)` are both linked dynamically, - // `libart(d)` must be specified in shared_libs list before `libnativehelper`, - // so that its symbols have precedence over `libnativehelper`. - "libnativehelper", + // The full libnativehelper library has wrappers for the JNI APIs (e.g. + // `JNI_CreateJavaVM`) that are implemented in libart(d), so it + // shouldn't be linked into the tests where libart(d) is linked + // statically. + "libnativehelper_header_only", ], target: { android: { @@ -956,6 +949,7 @@ cc_defaults { "720-thread-priority/thread_priority.cc", "800-smali/jni.cc", "817-hiddenapi/test_native.cc", + "855-native/throws_exception.cc", "909-attach-agent/disallow_debugging.cc", "993-breakpoints-non-debuggable/native_attach_agent.cc", "1001-app-image-regions/app_image_regions.cc", diff --git a/test/README.atest.md b/test/README.atest.md index c23a7d6624..9f88ee2f3f 100644 --- a/test/README.atest.md +++ b/test/README.atest.md @@ -142,6 +142,20 @@ This sequence: 5. cleans up the environment (deactivates the APEXes and removes the `chroot` environment). +### Running ART gtests on host + +You first need to build the boot classpath and boot image on host: + +```bash +m art-host-tests +``` + +Then you can use `atest --host` to run host gtests, e.g: + +```bash +atest --host art_runtime_tests +``` + ## Test Mapping ART Testing supports the execution of tests via [Test diff --git a/test/jvmti-common/Trace.java b/test/jvmti-common/Trace.java index 8999bb1368..3df1737c91 100644 --- a/test/jvmti-common/Trace.java +++ b/test/jvmti-common/Trace.java @@ -28,6 +28,7 @@ public class Trace { Method singleStep, Thread thr); public static native void disableTracing(Thread thr); + public static native void nativeEnableFramePopEvents(); public static void enableFieldTracing(Class<?> methodClass, Method fieldAccess, @@ -43,6 +44,10 @@ public class Trace { enableTracing(methodClass, entryMethod, exitMethod, null, null, null, thr); } + public static void enableFramePopEvents() { + nativeEnableFramePopEvents(); + } + public static void enableSingleStepTracing(Class<?> methodClass, Method singleStep, Thread thr) { diff --git a/test/knownfailures.json b/test/knownfailures.json index 4e5a19d0eb..dff43c55e8 100644 --- a/test/knownfailures.json +++ b/test/knownfailures.json @@ -1179,6 +1179,7 @@ "847-filled-new-aray", "848-pattern-match", "854-image-inlining", + "855-native", "999-redefine-hiddenapi", "1000-non-moving-space-stress", "1001-app-image-regions", diff --git a/test/odsign/test-src/com/android/tests/odsign/CompOsTestUtils.java b/test/odsign/test-src/com/android/tests/odsign/CompOsTestUtils.java index 4521b6fa59..91a6276845 100644 --- a/test/odsign/test-src/com/android/tests/odsign/CompOsTestUtils.java +++ b/test/odsign/test-src/com/android/tests/odsign/CompOsTestUtils.java @@ -120,7 +120,8 @@ public class CompOsTestUtils { // We have to have kernel support for a VM. assumeTrue("Need an actual TestDevice", mDevice instanceof TestDevice); TestDevice testDevice = (TestDevice) mDevice; - assumeTrue("Requires VM support", testDevice.supportsMicrodroid()); + assumeTrue("Requires protected VM support", + testDevice.supportsMicrodroid(true /* protectedVm*/)); // And the CompOS APEX must be present. assumeTrue(mDevice.doesFileExist("/apex/com.android.compos/")); diff --git a/test/ti-agent/trace_helper.cc b/test/ti-agent/trace_helper.cc index 58958cb268..e34b5bdce9 100644 --- a/test/ti-agent/trace_helper.cc +++ b/test/ti-agent/trace_helper.cc @@ -466,6 +466,15 @@ extern "C" JNIEXPORT void JNICALL Java_art_Trace_watchFieldAccess( env->DeleteLocalRef(klass); } +extern "C" JNIEXPORT void JNICALL Java_art_Trace_nativeEnableFramePopEvents(JNIEnv* env) { + jvmtiCapabilities caps; + memset(&caps, 0, sizeof(caps)); + caps.can_pop_frame = 1; + if (JvmtiErrorToException(env, jvmti_env, jvmti_env->AddCapabilities(&caps))) { + return; + } +} + extern "C" JNIEXPORT void JNICALL Java_art_Trace_enableTracing2( JNIEnv* env, [[maybe_unused]] jclass trace, diff --git a/test/utils/regen-test-files b/test/utils/regen-test-files index cedf354882..ed5aa45f6b 100755 --- a/test/utils/regen-test-files +++ b/test/utils/regen-test-files @@ -273,7 +273,6 @@ art_gtest_module_names = sorted(art_gtest_user_module_names + art_gtest_eng_only # removing them from this set (in order to promote them to # presubmits). art_gtest_postsubmit_only_module_names = [ - "libnativebridge-tests", ] # ART gtests supported in MTS that do not need root access to the device. @@ -698,6 +697,13 @@ class Generator: [], mainline_presubmit_apex_suffix) + # ART mainline presubmits tests without APEX suffix + art_mainline_presubmit_tests_dict = \ + gen_tests_dict(mainline_presubmit_tests, + failing_tests_excluded_from_mainline_presubmits, + [], + "") + # Android Virtualization Framework presubmits avf_presubmit_tests = ["ComposHostTestCases"] avf_presubmit_tests_dict = gen_tests_dict(avf_presubmit_tests, @@ -731,6 +737,7 @@ class Generator: (test_group_name, test_group_dict) for (test_group_name, test_group_dict) in [ + ("art-mainline-presubmit", art_mainline_presubmit_tests_dict), ("mainline-presubmit", mainline_presubmit_tests_dict), ("presubmit", presubmit_tests_dict), ("hwasan-presubmit", hwasan_presubmit_tests_dict), @@ -1054,6 +1061,8 @@ class Generator: f" of {len(run_tests)} ({expected_succeeding_tests_percentage}%):") for (num_tests, test_kind, tests_percentage, test_group_name) in [ (num_mainline_presubmit_run_tests, "ART run-tests", mainline_presubmit_run_tests_percentage, + "art-mainline-presubmit"), + (num_mainline_presubmit_run_tests, "ART run-tests", mainline_presubmit_run_tests_percentage, "mainline-presubmit"), (num_presubmit_run_tests, "ART run-tests", presubmit_run_tests_percentage, "presubmit"), (num_postsubmit_run_tests, "ART run-tests", postsubmit_run_tests_percentage, "postsubmit"), diff --git a/tools/ahat/Android.bp b/tools/ahat/Android.bp index 6476120879..540ce6670c 100644 --- a/tools/ahat/Android.bp +++ b/tools/ahat/Android.bp @@ -34,6 +34,9 @@ java_binary_host { // Use a relaxed version to allow distribution against older runtimes. java_version: "11", javacflags: ["-Xdoclint:all/protected"], + static_libs: [ + "guava", + ], } // --- ahat-test-dump.jar -------------- @@ -53,5 +56,6 @@ java_test_helper_library { host_supported: true, device_supported: false, name: "ahat-ri-test-dump", + java_version: "17", srcs: ["src/ri-test-dump/**/*.java"], } diff --git a/tools/ahat/src/main/com/android/ahat/BitmapHandler.java b/tools/ahat/src/main/com/android/ahat/BitmapHandler.java index fb81ad9dbb..7cdbcdf044 100644 --- a/tools/ahat/src/main/com/android/ahat/BitmapHandler.java +++ b/tools/ahat/src/main/com/android/ahat/BitmapHandler.java @@ -18,6 +18,7 @@ package com.android.ahat; import com.android.ahat.heapdump.AhatInstance; import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.AhatBitmapInstance; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import java.awt.image.BufferedImage; @@ -61,12 +62,12 @@ class BitmapHandler implements HttpHandler { Query query = new Query(exchange.getRequestURI()); long id = query.getLong("id", 0); AhatInstance inst = mSnapshot.findInstance(id); - if (inst == null) { + if (inst == null || !inst.isBitmapInstance()) { handle_404(exchange); return; } - AhatInstance.Bitmap bitmap = inst.asBitmap(); + AhatBitmapInstance.Bitmap bitmap = inst.asBitmapInstance().getBitmap(); if (bitmap == null) { handle_404(exchange); return; diff --git a/tools/ahat/src/main/com/android/ahat/OverviewHandler.java b/tools/ahat/src/main/com/android/ahat/OverviewHandler.java index c6f4a54080..90d44c3b47 100644 --- a/tools/ahat/src/main/com/android/ahat/OverviewHandler.java +++ b/tools/ahat/src/main/com/android/ahat/OverviewHandler.java @@ -17,11 +17,14 @@ package com.android.ahat; import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatBitmapInstance; import com.android.ahat.heapdump.AhatSnapshot; import com.android.ahat.heapdump.Reachability; import com.android.ahat.heapdump.Size; import java.io.File; import java.io.IOException; +import java.util.List; class OverviewHandler implements AhatHandler { @@ -57,6 +60,9 @@ class OverviewHandler implements AhatHandler { doc.section("Bytes Retained by Heap"); printHeapSizes(doc); + + doc.section("Heap Analysis Result"); + printDuplicateBitmaps(doc); } private void printHeapSizes(Doc doc) { @@ -75,5 +81,30 @@ class OverviewHandler implements AhatHandler { SizeTable.row(doc, DocString.text("Total"), totalSize, totalBase); SizeTable.end(doc); } + + private void printDuplicateBitmaps(Doc doc) { + List<List<AhatBitmapInstance>> duplicates = mSnapshot.findDuplicateBitmaps(); + if (duplicates != null && duplicates.size() > 0) { + SizeTable.table(doc, mSnapshot.isDiffed(), + new Column("Heap"), + new Column("Duplicated Bitmaps")); + Size totalSize = Size.ZERO; + Size totalBase = Size.ZERO; + for (List<AhatBitmapInstance> list : duplicates) { + for (AhatBitmapInstance inst : list) { + AhatInstance base = inst.getBaseline(); + SizeTable.row(doc, inst.getSize(), base.getSize(), + DocString.text(inst.getHeap().getName()), + Summarizer.summarize(inst)); + totalSize = totalSize.plus(inst.getSize()); + totalBase = totalBase.plus(base.getSize()); + } + } + SizeTable.row(doc, totalSize, totalBase, + DocString.text("Total"), + DocString.text("All duplicated bitmaps")); + SizeTable.end(doc); + } + } } diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatBitmapInstance.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatBitmapInstance.java new file mode 100644 index 0000000000..982591d1e4 --- /dev/null +++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatBitmapInstance.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.ArrayListMultimap; + +/** + * A java object that has `android.graphics.Bitmap` as its base class. + */ +public class AhatBitmapInstance extends AhatClassInstance { + + private BitmapInfo mBitmapInfo = null; + + AhatBitmapInstance(long id) { + super(id); + } + + @Override + public boolean isBitmapInstance() { + return true; + } + + @Override + public AhatBitmapInstance asBitmapInstance() { + return this; + } + + /** + * Parsed information for bitmap contents dumped in the heapdump + */ + public static class BitmapDumpData { + private int count; + // See android.graphics.Bitmap.CompressFormat for format values. + // -1 means no compression for backward compatibility + private int format; + private Map<Long, byte[]> buffers; + private Set<Long> referenced; + private ListMultimap<BitmapInfo, AhatBitmapInstance> instances; + + BitmapDumpData(int count, int format) { + this.count = count; + this.format = format; + this.buffers = new HashMap<Long, byte[]>(count); + this.referenced = new HashSet<Long>(count); + this.instances = ArrayListMultimap.create(); + } + }; + + /** + * find the BitmapDumpData that is included in the heap dump + * + * @param root root of the heap dump + * @param instances all the instances from where the bitmap dump data will be excluded + * @return true if valid bitmap dump data is found, false if not + */ + public static BitmapDumpData findBitmapDumpData(SuperRoot root, Instances<AhatInstance> instances) { + final BitmapDumpData result; + AhatClassObj cls = null; + + for (Reference ref : root.getReferences()) { + if (ref.ref.isClassObj()) { + cls = ref.ref.asClassObj(); + if (cls.getName().equals("android.graphics.Bitmap")) { + break; + } + } + } + + if (cls == null) { + return null; + } + + Value value = cls.getStaticField("dumpData"); + if (value == null || !value.isAhatInstance()) { + return null; + } + + AhatClassInstance inst = value.asAhatInstance().asClassInstance(); + if (inst == null) { + return null; + } + + result = toBitmapDumpData(inst); + if (result == null) { + return null; + } + + /* Build the map for all the bitmap instances with its BitmapInfo as key, + * the map would be used to identify duplicated bitmaps later. This also + * initializes `mBitmapInfo` of each bitmap instance. + */ + for (AhatInstance obj : instances) { + AhatBitmapInstance bmp = obj.asBitmapInstance(); + if (bmp != null) { + BitmapInfo info = bmp.getBitmapInfo(result); + if (info != null) { + result.instances.put(info, bmp); + } + } + } + + /* remove all instances referenced from BitmapDumpData, + * these instances shall *not* be counted + */ + instances.removeIf(i -> { return result.referenced.contains(i.getId()); }); + return result; + } + + private static BitmapDumpData toBitmapDumpData(AhatClassInstance inst) { + if (!inst.isInstanceOfClass("android.graphics.Bitmap$DumpData")) { + return null; + } + + int count = inst.getIntField("count", 0); + int format = inst.getIntField("format", -1); + + if (count == 0 || format == -1) { + return null; + } + + BitmapDumpData result = new BitmapDumpData(count, format); + + AhatArrayInstance natives = inst.getArrayField("natives"); + AhatArrayInstance buffers = inst.getArrayField("buffers"); + if (natives == null || buffers == null) { + return null; + } + + result.referenced.add(natives.getId()); + result.referenced.add(buffers.getId()); + + result.buffers = new HashMap<>(result.count); + for (int i = 0; i < result.count; i++) { + Value nativePtr = natives.getValue(i); + Value bufferVal = buffers.getValue(i); + if (nativePtr == null || bufferVal == null) { + continue; + } + AhatInstance buffer = bufferVal.asAhatInstance(); + result.buffers.put(nativePtr.asLong(), buffer.asArrayInstance().asByteArray()); + result.referenced.add(buffer.getId()); + } + return result; + } + + /** + * find duplicated bitmap instances + * + * @param bitmapDumpData parsed bitmap dump data + * @return A list of duplicated bitmaps (the same duplication stored in a sub-list) + */ + public static List<List<AhatBitmapInstance>> findDuplicates(BitmapDumpData bitmapDumpData) { + if (bitmapDumpData != null) { + List<List<AhatBitmapInstance>> result = new ArrayList<>(); + for (BitmapInfo info : bitmapDumpData.instances.keySet()) { + List<AhatBitmapInstance> list = bitmapDumpData.instances.get(info); + if (list != null && list.size() > 1) { + result.add(list); + } + } + // sort by size in descend order + if (result.size() > 1) { + result.sort((List<AhatBitmapInstance> l1, List<AhatBitmapInstance> l2) -> { + return l2.get(0).getSize().compareTo(l1.get(0).getSize()); + }); + } + return result; + } + return null; + } + + private static class BitmapInfo { + private final int width; + private final int height; + private final int format; + private final byte[] buffer; + private final int bufferHash; + + public BitmapInfo(int width, int height, int format, byte[] buffer) { + this.width = width; + this.height = height; + this.format = format; + this.buffer = buffer; + bufferHash = Arrays.hashCode(buffer); + } + + @Override + public int hashCode() { + return Objects.hash(width, height, format, bufferHash); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof BitmapInfo)) { + return false; + } + BitmapInfo other = (BitmapInfo)o; + return (this.width == other.width) + && (this.height == other.height) + && (this.format == other.format) + && (this.bufferHash == other.bufferHash); + } + } + + /** + * Return bitmap info for this object, or null if no appropriate bitmap + * info is available. + */ + private BitmapInfo getBitmapInfo(BitmapDumpData bitmapDumpData) { + if (mBitmapInfo != null) { + return mBitmapInfo; + } + + if (!isInstanceOfClass("android.graphics.Bitmap")) { + return null; + } + + Integer width = getIntField("mWidth", null); + if (width == null) { + return null; + } + + Integer height = getIntField("mHeight", null); + if (height == null) { + return null; + } + + byte[] buffer = getByteArrayField("mBuffer"); + if (buffer != null) { + if (buffer.length < 4 * height * width) { + return null; + } + mBitmapInfo = new BitmapInfo(width, height, -1, buffer); + return mBitmapInfo; + } + + long nativePtr = getLongField("mNativePtr", -1l); + if (nativePtr == -1) { + return null; + } + + if (bitmapDumpData == null || bitmapDumpData.count == 0) { + return null; + } + + if (!bitmapDumpData.buffers.containsKey(nativePtr)) { + return null; + } + + buffer = bitmapDumpData.buffers.get(nativePtr); + if (buffer == null) { + return null; + } + + mBitmapInfo = new BitmapInfo(width, height, bitmapDumpData.format, buffer); + return mBitmapInfo; + } + + /** + * Represents a bitmap with either + * - its format and content in `buffer` + * - or a BufferedImage with its raw pixels + */ + public static class Bitmap { + /** + * format of the bitmap content in buffer + */ + public String format; + /** + * byte buffer of the bitmap content + */ + public byte[] buffer; + /** + * BufferedImage with the bitmap's raw pixels + */ + public BufferedImage image; + + /** + * Initialize a Bitmap instance + * @param format - format of the bitmap + * @param buffer - buffer of the bitmap content + * @param image - BufferedImage with the bitmap's raw pixel + */ + public Bitmap(String format, byte[] buffer, BufferedImage image) { + this.format = format; + this.buffer = buffer; + this.image = image; + } + } + + private BufferedImage asBufferedImage(BitmapInfo info) { + // Convert the raw data to an image + // Convert BGRA to ABGR + int[] abgr = new int[info.height * info.width]; + for (int i = 0; i < abgr.length; i++) { + abgr[i] = ( + (((int) info.buffer[i * 4 + 3] & 0xFF) << 24) + + (((int) info.buffer[i * 4 + 0] & 0xFF) << 16) + + (((int) info.buffer[i * 4 + 1] & 0xFF) << 8) + + ((int) info.buffer[i * 4 + 2] & 0xFF)); + } + + BufferedImage bitmap = new BufferedImage( + info.width, info.height, BufferedImage.TYPE_4BYTE_ABGR); + bitmap.setRGB(0, 0, info.width, info.height, abgr, 0, info.width); + return bitmap; + } + + /** + * Returns the bitmap associated with this instance. + * This is relevant for instances of android.graphics.Bitmap. + * Returns null if there is no bitmap pixel data associated + * with the given instance. + * + * @return the bitmap pixel data associated with this image + */ + public Bitmap getBitmap() { + final BitmapInfo info = mBitmapInfo; + if (info == null) { + return null; + } + + /** + * See android.graphics.Bitmap.CompressFormat for definitions + * -1 for legacy objects with content in `Bitmap.mBuffer` + */ + switch (info.format) { + case 0: /* JPEG */ + return new Bitmap("image/jpg", info.buffer, null); + case 1: /* PNG */ + return new Bitmap("image/png", info.buffer, null); + case 2: /* WEBP */ + case 3: /* WEBP_LOSSY */ + case 4: /* WEBP_LOSSLESS */ + return new Bitmap("image/webp", info.buffer, null); + case -1:/* Legacy */ + return new Bitmap(null, null, asBufferedImage(info)); + default: + return null; + } + } + +} diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassInstance.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassInstance.java index 07e8cf9771..d193f08848 100644 --- a/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassInstance.java +++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassInstance.java @@ -16,9 +16,6 @@ package com.android.ahat.heapdump; -import java.awt.image.BufferedImage; -import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; @@ -32,6 +29,21 @@ import java.util.NoSuchElementException; * object in addition to those methods inherited from {@link AhatInstance}. */ public class AhatClassInstance extends AhatInstance { + + /** + * Create an AhatClassInstance or AhatBitmapInstance if it's a subclass of + * android.graphics.Bitmap + * + * @param classObj - the class of this object + * @param objectId - the object Id + * @return an AhatClassInstance or AhatBitmapInstance + */ + public static AhatClassInstance create(AhatClassObj classObj, long objectId) { + return classObj.isSubClassOf("android.graphics.Bitmap") + ? new AhatBitmapInstance(objectId) + : new AhatClassInstance(objectId); + } + // Instance fields of the object. These are stored in order of the instance // field descriptors from the class object, starting with this class first, // followed by the super class, and so on. We store the values separate from @@ -68,10 +80,13 @@ public class AhatClassInstance extends AhatInstance { /** * Read an int field of an instance. * The field is assumed to be an int type. - * Returns <code>def</code> if the field value is not an int or could not be + * + * @param fieldName name of the int field + * @param def default value if the field is not an int or could not be read + * @return <code>def</code> if the field value is not an int or could not be * read. */ - private Integer getIntField(String fieldName, Integer def) { + protected Integer getIntField(String fieldName, Integer def) { Value value = getField(fieldName); if (value == null || !value.isInteger()) { return def; @@ -82,10 +97,13 @@ public class AhatClassInstance extends AhatInstance { /** * Read a long field of this instance. * The field is assumed to be a long type. - * Returns <code>def</code> if the field value is not an long or could not + * + * @param fieldName name of the long field + * @param def default value if the field is not an int or could not be read + * @return <code>def</code> if the field value is not an long or could not * be read. */ - private Long getLongField(String fieldName, Long def) { + protected Long getLongField(String fieldName, Long def) { Value value = getField(fieldName); if (value == null || !value.isLong()) { return def; @@ -109,6 +127,30 @@ public class AhatClassInstance extends AhatInstance { return new ReferenceIterator(); } + /** + * Returns the value of the field of `fieldName` as an AhatArrayInstance + * + * @param fieldName name of the array field + * @return null if the field is not found, or the field is not an + * AhatArrayInstance. + */ + protected AhatArrayInstance getArrayField(String fieldName) { + AhatInstance field = getRefField(fieldName); + return (field == null) ? null : field.asArrayInstance(); + } + + /** + * Reads the given field from the given instance. + * The field is assumed to be a byte[] field. + * + * @param fieldName name of the byte array field + * @return null if the field value is null, not a byte[] or could not be read. + */ + protected byte[] getByteArrayField(String fieldName) { + AhatInstance field = getRefField(fieldName); + return field == null ? null : field.asByteArray(); + } + @Override public String asString(int maxChars) { if (!isInstanceOfClass("java.lang.String")) { return null; @@ -194,7 +236,7 @@ public class AhatClassInstance extends AhatInstance { } @Override public AhatInstance getAssociatedBitmapInstance() { - return getBitmapInfo() == null ? null : this; + return asBitmapInstance(); } @Override public boolean isClassInstance() { @@ -228,235 +270,6 @@ public class AhatClassInstance extends AhatInstance { } } - /** - * Returns the value of the field of `fieldName` as an AhatArrayInstance - * Returns null if the field is not found, or the field is not an - * AhatArrayInstance. - */ - private AhatArrayInstance getArrayField(String fieldName) { - AhatInstance field = getRefField(fieldName); - return (field == null) ? null : field.asArrayInstance(); - } - - /** - * Read the given field from the given instance. - * The field is assumed to be a byte[] field. - * Returns null if the field value is null, not a byte[] or could not be read. - */ - private byte[] getByteArrayField(String fieldName) { - AhatInstance field = getRefField(fieldName); - return field == null ? null : field.asByteArray(); - } - - private static class BitmapDumpData { - public int count; - // See android.graphics.Bitmap.CompressFormat for format values. - // -1 means no compression for backward compatibility - public int format; - public HashMap<Long, byte[]> buffers; - public HashSet<Long> referenced; - - public BitmapDumpData(int count, int format) { - this.count = count; - this.format = format; - this.buffers = new HashMap<Long, byte[]>(count); - this.referenced = new HashSet<Long>(count); - } - }; - - private static BitmapDumpData bitmapDumpData = null; - - /** - * find the BitmapDumpData that is included in the heap dump - * - * @param root root of the heap dump - * @param instances all the instances from where the bitmap dump data will be excluded - * @return true if valid bitmap dump data is found, false if not - */ - public static boolean findBitmapDumpData(SuperRoot root, Instances<AhatInstance> instances) { - final BitmapDumpData result; - AhatClassObj cls = null; - - for (Reference ref : root.getReferences()) { - if (ref.ref.isClassObj()) { - cls = ref.ref.asClassObj(); - if (cls.getName().equals("android.graphics.Bitmap")) { - break; - } - } - } - - if (cls == null) { - return false; - } - - Value value = cls.getStaticField("dumpData"); - if (value == null || !value.isAhatInstance()) { - return false; - } - - AhatClassInstance inst = value.asAhatInstance().asClassInstance(); - if (inst == null) { - return false; - } - - result = inst.asBitmapDumpData(); - if (result == null) { - return false; - } - - /* remove all instances referenced from BitmapDumpData, - * these instances shall *not* be counted - */ - final HashSet<Long> referenced = result.referenced; - instances.removeIf(i -> { return referenced.contains(i.getId()); }); - bitmapDumpData = result; - return true; - } - - private BitmapDumpData asBitmapDumpData() { - if (!isInstanceOfClass("android.graphics.Bitmap$DumpData")) { - return null; - } - - int count = getIntField("count", 0); - int format = getIntField("format", -1); - - if (count == 0 || format == -1) { - return null; - } - - BitmapDumpData result = new BitmapDumpData(count, format); - - AhatArrayInstance natives = getArrayField("natives"); - AhatArrayInstance buffers = getArrayField("buffers"); - if (natives == null || buffers == null) { - return null; - } - - result.referenced.add(natives.getId()); - result.referenced.add(buffers.getId()); - - result.buffers = new HashMap<>(result.count); - for (int i = 0; i < result.count; i++) { - Value nativePtr = natives.getValue(i); - Value bufferVal = buffers.getValue(i); - if (nativePtr == null || bufferVal == null) { - continue; - } - AhatInstance buffer = bufferVal.asAhatInstance(); - result.buffers.put(nativePtr.asLong(), buffer.asArrayInstance().asByteArray()); - result.referenced.add(buffer.getId()); - } - return result; - } - - private static class BitmapInfo { - public final int width; - public final int height; - public final int format; - public final byte[] buffer; - - public BitmapInfo(int width, int height, int format, byte[] buffer) { - this.width = width; - this.height = height; - this.format = format; - this.buffer = buffer; - } - } - - /** - * Return bitmap info for this object, or null if no appropriate bitmap - * info is available. - */ - private BitmapInfo getBitmapInfo() { - if (!isInstanceOfClass("android.graphics.Bitmap")) { - return null; - } - - Integer width = getIntField("mWidth", null); - if (width == null) { - return null; - } - - Integer height = getIntField("mHeight", null); - if (height == null) { - return null; - } - - byte[] buffer = getByteArrayField("mBuffer"); - if (buffer != null) { - if (buffer.length < 4 * height * width) { - return null; - } - return new BitmapInfo(width, height, -1, buffer); - } - - long nativePtr = getLongField("mNativePtr", -1l); - if (nativePtr == -1) { - return null; - } - - if (bitmapDumpData == null || bitmapDumpData.count == 0) { - return null; - } - - if (!bitmapDumpData.buffers.containsKey(nativePtr)) { - return null; - } - - buffer = bitmapDumpData.buffers.get(nativePtr); - if (buffer == null) { - return null; - } - - return new BitmapInfo(width, height, bitmapDumpData.format, buffer); - } - - private BufferedImage asBufferedImage(BitmapInfo info) { - // Convert the raw data to an image - // Convert BGRA to ABGR - int[] abgr = new int[info.height * info.width]; - for (int i = 0; i < abgr.length; i++) { - abgr[i] = ( - (((int) info.buffer[i * 4 + 3] & 0xFF) << 24) - + (((int) info.buffer[i * 4 + 0] & 0xFF) << 16) - + (((int) info.buffer[i * 4 + 1] & 0xFF) << 8) - + ((int) info.buffer[i * 4 + 2] & 0xFF)); - } - - BufferedImage bitmap = new BufferedImage( - info.width, info.height, BufferedImage.TYPE_4BYTE_ABGR); - bitmap.setRGB(0, 0, info.width, info.height, abgr, 0, info.width); - return bitmap; - } - - @Override public Bitmap asBitmap() { - BitmapInfo info = getBitmapInfo(); - if (info == null) { - return null; - } - - /** - * See android.graphics.Bitmap.CompressFormat for definitions - * -1 for legacy objects with content in `Bitmap.mBuffer` - */ - switch (info.format) { - case 0: /* JPEG */ - return new Bitmap("image/jpg", info.buffer, null); - case 1: /* PNG */ - return new Bitmap("image/png", info.buffer, null); - case 2: /* WEBP */ - case 3: /* WEBP_LOSSY */ - case 4: /* WEBP_LOSSLESS */ - return new Bitmap("image/webp", info.buffer, null); - case -1:/* Legacy */ - return new Bitmap(null, null, asBufferedImage(info)); - default: - return null; - } - } - @Override RegisteredNativeAllocation asRegisteredNativeAllocation() { if (!isInstanceOfClass("sun.misc.Cleaner")) { diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassObj.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassObj.java index 68b400f501..0294f2eee0 100644 --- a/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassObj.java +++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatClassObj.java @@ -162,6 +162,23 @@ public class AhatClassObj extends AhatInstance { return true; } + /** + * Returns true if this is a subclass of another class with the given name. + * + * @param className the name of the class to check for subclass + * @return true if this is a subclass of another class with the given name + */ + public boolean isSubClassOf(String className) { + AhatClassObj cls = this; + while (cls != null) { + if (className.equals(cls.getName())) { + return true; + } + cls = cls.getSuperClassObj(); + } + return false; + } + @Override public AhatClassObj asClassObj() { return this; } diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java index 1278c584c3..8358553722 100644 --- a/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java +++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java @@ -392,6 +392,24 @@ public abstract class AhatInstance implements Diffable<AhatInstance> { } /** + * Returns true if this instance is a bitmap instance. + * @return true if this instance is a bitmap instance + */ + public boolean isBitmapInstance() { + return false; + } + + /** + * Returns this as an AhatBitmapInstance if this is an AhatBitmapInstance. + * Returns null if this is not an AhatBitmapInstance. + * + * @return this instance as a bitmap instance + */ + public AhatBitmapInstance asBitmapInstance() { + return null; + } + + /** * Returns the <code>referent</code> associated with this instance. * This is only relevant for instances of java.lang.ref.Reference or its * subclasses. Returns null if the instance has no referent associated with @@ -583,50 +601,6 @@ public abstract class AhatInstance implements Diffable<AhatInstance> { return asString(-1); } - /** - * Represents a bitmap with either - * - its format and content in `buffer` - * - or a BufferedImage with its raw pixels - */ - public static class Bitmap { - /** - * format of the bitmap content in buffer - */ - public String format; - /** - * byte buffer of the bitmap content - */ - public byte[] buffer; - /** - * BufferedImage with the bitmap's raw pixels - */ - public BufferedImage image; - - /** - * Initialize a Bitmap instance - * @param format - format of the bitmap - * @param buffer - buffer of the bitmap content - * @param image - BufferedImage with the bitmap's raw pixel - */ - public Bitmap(String format, byte[] buffer, BufferedImage image) { - this.format = format; - this.buffer = buffer; - this.image = image; - } - } - - /** - * Returns the bitmap associated with this instance. - * This is relevant for instances of android.graphics.Bitmap and byte[]. - * Returns null if there is no bitmap pixel data associated with the given - * instance. - * - * @return the bitmap pixel data associated with this image - */ - public Bitmap asBitmap() { - return null; - } - static class RegisteredNativeAllocation { public AhatInstance referent; public long size; diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java index 16c1e2f576..8f205bfb55 100644 --- a/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java +++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java @@ -37,6 +37,8 @@ public class AhatSnapshot implements Diffable<AhatSnapshot> { private AhatSnapshot mBaseline = this; + private AhatBitmapInstance.BitmapDumpData mBitmapDumpData = null; + AhatSnapshot(SuperRoot root, Instances<AhatInstance> instances, List<AhatHeap> heaps, @@ -49,7 +51,8 @@ public class AhatSnapshot implements Diffable<AhatSnapshot> { mRootSite = rootSite; AhatInstance.computeReachability(mSuperRoot, progress, mInstances.size()); - AhatClassInstance.findBitmapDumpData(mSuperRoot, mInstances); + + mBitmapDumpData = AhatBitmapInstance.findBitmapDumpData(mSuperRoot, mInstances); for (AhatInstance inst : mInstances) { // Add this instance to its site. @@ -209,4 +212,8 @@ public class AhatSnapshot implements Diffable<AhatSnapshot> { @Override public boolean isPlaceHolder() { return false; } + + public List<List<AhatBitmapInstance>> findDuplicateBitmaps() { + return AhatBitmapInstance.findDuplicates(mBitmapDumpData); + } } diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java b/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java index 7fe444e035..f28792b53d 100644 --- a/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java +++ b/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java @@ -400,7 +400,7 @@ public class Parser { Site site = sites.get(stackSerialNumber); AhatClassObj classObj = classById.get(classId); - AhatClassInstance obj = new AhatClassInstance(objectId); + AhatClassInstance obj = AhatClassInstance.create(classObj, objectId); obj.initialize(heaps.getCurrentHeap(), site, classObj); obj.setTemporaryUserData(data); instances.add(obj); diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/Size.java b/tools/ahat/src/main/com/android/ahat/heapdump/Size.java index b721bac6ba..c5103e46d0 100644 --- a/tools/ahat/src/main/com/android/ahat/heapdump/Size.java +++ b/tools/ahat/src/main/com/android/ahat/heapdump/Size.java @@ -16,6 +16,7 @@ package com.android.ahat.heapdump; +import java.lang.Comparable; import java.util.Objects; /** @@ -27,7 +28,7 @@ import java.util.Objects; * <p> * Size objects are immutable. */ -public class Size { +public class Size implements Comparable<Size> { private final long mJavaSize; private final long mRegisteredNativeSize; @@ -124,5 +125,10 @@ public class Size { } return false; } + + @Override public int compareTo(Size other) { + return Long.compare(this.mJavaSize + this.mRegisteredNativeSize, + other.mJavaSize + other.mRegisteredNativeSize); + } } diff --git a/tools/ahat/src/test-dump/DumpedStuff.java b/tools/ahat/src/test-dump/DumpedStuff.java index de2968f51b..2b58ab68d9 100644 --- a/tools/ahat/src/test-dump/DumpedStuff.java +++ b/tools/ahat/src/test-dump/DumpedStuff.java @@ -19,6 +19,7 @@ import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import libcore.util.NativeAllocationRegistry; +import android.graphics.Bitmap; // We take a heap dump that includes a single instance of this // DumpedStuff class. Objects stored as fields in this class can be easily @@ -85,6 +86,9 @@ public class DumpedStuff extends SuperDumpedStuff { } gcPathArray[2].right.left = gcPathArray[2].left.right; + + bitmapOne = new Bitmap(100, 200, 0xDEADBEEF, bigArray); + bitmapTwo = new Bitmap(100, 200, 0xBEEFDEAD, bigArray); } public static class ObjectTree { @@ -179,6 +183,8 @@ public class DumpedStuff extends SuperDumpedStuff { public SoftReference aSoftReference = new SoftReference(new Object()); public Reference reachabilityReferenceChain; public byte[] bigArray; + public Bitmap bitmapOne = null; + public Bitmap bitmapTwo = null; public ObjectTree[] gcPathArray = new ObjectTree[]{null, null, new ObjectTree( new ObjectTree(null, new ObjectTree(null, null)), diff --git a/tools/ahat/src/test-dump/android/graphics/Bitmap.java b/tools/ahat/src/test-dump/android/graphics/Bitmap.java new file mode 100644 index 0000000000..dadc5010c6 --- /dev/null +++ b/tools/ahat/src/test-dump/android/graphics/Bitmap.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +/** + * Fake android.graphics.Bitmap class that's minimum for testing + */ +public final class Bitmap { + + private final long mNativePtr; + + private final int mWidth; + private final int mHeight; + + public Bitmap(int width, int height, long nativePtr, byte[] buffer) { + this.mWidth = width; + this.mHeight = height; + this.mNativePtr = nativePtr; + dumpData.add(nativePtr, buffer); + } + + private static final class DumpData { + private final int format; + private final long[] natives; + private final byte[][] buffers; + private final int max; + private int count; + + public DumpData(int format, int max) { + this.max = max; + this.format = format; + this.natives = new long[max]; + this.buffers = new byte[max][]; + this.count = 0; + } + + public void add(long nativePtr, byte[] buffer) { + natives[count] = nativePtr; + buffers[count] = buffer; + count = (count >= max) ? max : count + 1; + } + }; + + // assume default format 'PNG' and maximum 10 test bitmaps + private static DumpData dumpData = new DumpData(1, 10); +} diff --git a/tools/ahat/src/test/com/android/ahat/InstanceTest.java b/tools/ahat/src/test/com/android/ahat/InstanceTest.java index 1f290304c0..e1eb504c1f 100644 --- a/tools/ahat/src/test/com/android/ahat/InstanceTest.java +++ b/tools/ahat/src/test/com/android/ahat/InstanceTest.java @@ -19,6 +19,7 @@ package com.android.ahat; import com.android.ahat.heapdump.AhatClassObj; import com.android.ahat.heapdump.AhatHeap; import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatBitmapInstance; import com.android.ahat.heapdump.AhatSnapshot; import com.android.ahat.heapdump.PathElement; import com.android.ahat.heapdump.Reachability; @@ -28,6 +29,7 @@ import java.io.IOException; import java.util.List; import org.junit.Test; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -359,21 +361,24 @@ public class InstanceTest { public void objectNotABitmap() throws IOException { TestDump dump = TestDump.getTestDump(); AhatInstance obj = dump.getDumpedAhatInstance("anObject"); - assertNull(obj.asBitmap()); + assertFalse(obj.isBitmapInstance()); + assertNull(obj.asBitmapInstance()); } @Test public void arrayNotABitmap() throws IOException { TestDump dump = TestDump.getTestDump(); AhatInstance obj = dump.getDumpedAhatInstance("gcPathArray"); - assertNull(obj.asBitmap()); + assertFalse(obj.isBitmapInstance()); + assertNull(obj.asBitmapInstance()); } @Test public void classObjNotABitmap() throws IOException { TestDump dump = TestDump.getTestDump(); AhatInstance obj = dump.findClass("Main"); - assertNull(obj.asBitmap()); + assertFalse(obj.isBitmapInstance()); + assertNull(obj.asBitmapInstance()); } @Test @@ -413,6 +418,36 @@ public class InstanceTest { } @Test + public void asBitmap() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("bitmapOne"); + assertNotNull(obj); + assertTrue(obj.isBitmapInstance()); + assertNotNull(obj.asBitmapInstance()); + + AhatBitmapInstance.Bitmap bmp = obj.asBitmapInstance().getBitmap(); + assertNotNull(bmp); + + // default compression format + assertEquals(bmp.format, "image/png"); + + // check buffer content - expect to be same as in bigArray + assertEquals(bmp.buffer.length, 1000000); + for (int i = 0; i < 1000000; i++) { + assertEquals(bmp.buffer[i], (byte)((i * i) & 0xFF)); + } + } + + @Test + public void duplicatedBitmaps() throws IOException { + TestDump dump = TestDump.getTestDump(); + List<List<AhatBitmapInstance>> duplicates = dump.getAhatSnapshot().findDuplicateBitmaps(); + // expect to have 1 list of 2 duplicated bitmaps + assertEquals(duplicates.size(), 1); + assertEquals(duplicates.get(0).size(), 2); + } + + @Test public void isRoot() throws IOException { // We expect the Main class to be a root. TestDump dump = TestDump.getTestDump(); diff --git a/tools/ahat/src/test/com/android/ahat/SiteTest.java b/tools/ahat/src/test/com/android/ahat/SiteTest.java index 78ef9b3c60..1f3331ae1c 100644 --- a/tools/ahat/src/test/com/android/ahat/SiteTest.java +++ b/tools/ahat/src/test/com/android/ahat/SiteTest.java @@ -37,20 +37,20 @@ public class SiteTest { Site sKnownSite = oKnownSite.getSite(); assertEquals("DumpedStuff.java", sKnownSite.getFilename()); assertEquals("allocateObjectAtKnownSite", sKnownSite.getMethodName()); - assertEquals(29, sKnownSite.getLineNumber()); + assertEquals(30, sKnownSite.getLineNumber()); assertSame(sKnownSite, snapshot.getSite(sKnownSite.getId())); AhatInstance oKnownSubSite = dump.getDumpedAhatInstance("objectAllocatedAtKnownSubSite"); Site sKnownSubSite = oKnownSubSite.getSite(); assertEquals("DumpedStuff.java", sKnownSubSite.getFilename()); assertEquals("allocateObjectAtKnownSubSite", sKnownSubSite.getMethodName()); - assertEquals(37, sKnownSubSite.getLineNumber()); + assertEquals(38, sKnownSubSite.getLineNumber()); assertSame(sKnownSubSite, snapshot.getSite(sKnownSubSite.getId())); Site sKnownSubSiteParent = sKnownSubSite.getParent(); assertEquals("DumpedStuff.java", sKnownSubSiteParent.getFilename()); assertEquals("allocateObjectAtKnownSite", sKnownSubSiteParent.getMethodName()); - assertEquals(30, sKnownSubSiteParent.getLineNumber()); + assertEquals(31, sKnownSubSiteParent.getLineNumber()); assertSame(sKnownSubSiteParent, snapshot.getSite(sKnownSubSiteParent.getId())); assertNotSame(sKnownSite, sKnownSubSiteParent); @@ -59,7 +59,7 @@ public class SiteTest { Site sKnownSiteParent = sKnownSite.getParent(); assertEquals("DumpedStuff.java", sKnownSiteParent.getFilename()); assertEquals("<init>", sKnownSiteParent.getMethodName()); - assertEquals(45, sKnownSiteParent.getLineNumber()); + assertEquals(46, sKnownSiteParent.getLineNumber()); assertSame(sKnownSiteParent, snapshot.getSite(sKnownSiteParent.getId())); AhatInstance oObfSuperSite = dump.getDumpedAhatInstance("objectAllocatedAtObfSuperSite"); @@ -80,7 +80,7 @@ public class SiteTest { Site sOverriddenSite = oOverriddenSite.getSite(); assertEquals("DumpedStuff.java", sOverriddenSite.getFilename()); assertEquals("allocateObjectAtOverriddenSite", sOverriddenSite.getMethodName()); - assertEquals(41, sOverriddenSite.getLineNumber()); + assertEquals(42, sOverriddenSite.getLineNumber()); assertSame(sOverriddenSite, snapshot.getSite(sOverriddenSite.getId())); } diff --git a/tools/checker/README b/tools/checker/README index e01f0d0750..a87a72be77 100644 --- a/tools/checker/README +++ b/tools/checker/README @@ -96,12 +96,12 @@ CHECK-IF and CHECK-ELIF take a Python expression as input that will be evaluated A possible use case of branching is to check whether the generated code exploits the instruction architecture features enabled at compile time. For that purpose, you can call the custom made -function isaHasFeature("feature_name"). +function hasIsaFeature("feature_name"). Example: /// CHECK-START-ARM64: int other.TestByte.testDotProdComplex(byte[], byte[]) disassembly (after) /// CHECK: VecDotProd - /// CHECK-IF: isaHasFeature("dotprod") + /// CHECK-IF: hasIsaFeature("dotprod") /// CHECK: sdot /// CHECK-ELSE: /// CHECK-NOT: sdot diff --git a/tools/hiddenapi/Android.bp b/tools/hiddenapi/Android.bp index fff94926ab..506197ce46 100644 --- a/tools/hiddenapi/Android.bp +++ b/tools/hiddenapi/Android.bp @@ -41,7 +41,7 @@ cc_defaults { stl: "c++_static", static_libs: [ "libbase", - "libcrypto", + "libcrypto_for_art", ], } |