/* * Copyright (C) 2014 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 "transaction.h" #include #include "aot_class_linker.h" #include "base/mutex-inl.h" #include "base/stl_util.h" #include "dex/descriptors_names.h" #include "gc/accounting/card_table-inl.h" #include "gc/heap.h" #include "gc_root-inl.h" #include "intern_table.h" #include "mirror/class-inl.h" #include "mirror/dex_cache-inl.h" #include "mirror/object-inl.h" #include "mirror/object_array-inl.h" #include "obj_ptr-inl.h" #include "runtime.h" #include namespace art { // TODO: remove (only used for debugging purpose). static constexpr bool kEnableTransactionStats = false; Transaction::Transaction(bool strict, mirror::Class* root, ArenaStack* arena_stack, ArenaPool* arena_pool) : arena_stack_(std::nullopt), allocator_(arena_stack != nullptr ? arena_stack : &arena_stack_.emplace(arena_pool)), object_logs_(std::less(), allocator_.Adapter(kArenaAllocTransaction)), array_logs_(std::less(), allocator_.Adapter(kArenaAllocTransaction)), intern_string_logs_(allocator_.Adapter(kArenaAllocTransaction)), resolve_string_logs_(allocator_.Adapter(kArenaAllocTransaction)), resolve_method_type_logs_(allocator_.Adapter(kArenaAllocTransaction)), aborted_(false), rolling_back_(false), heap_(Runtime::Current()->GetHeap()), strict_(strict), root_(root), assert_no_new_records_reason_(nullptr) { DCHECK(Runtime::Current()->IsAotCompiler()); DCHECK_NE(arena_stack != nullptr, arena_pool != nullptr); } Transaction::~Transaction() { if (kEnableTransactionStats) { size_t objects_count = object_logs_.size(); size_t field_values_count = 0; for (const auto& it : object_logs_) { field_values_count += it.second.Size(); } size_t array_count = array_logs_.size(); size_t array_values_count = 0; for (const auto& it : array_logs_) { array_values_count += it.second.Size(); } size_t intern_string_count = std::distance(intern_string_logs_.begin(), intern_string_logs_.end()); size_t resolve_string_count = std::distance(resolve_string_logs_.begin(), resolve_string_logs_.end()); size_t resolve_method_type_count = std::distance(resolve_method_type_logs_.begin(), resolve_method_type_logs_.end()); LOG(INFO) << "Transaction::~Transaction" << ": objects_count=" << objects_count << ", field_values_count=" << field_values_count << ", array_count=" << array_count << ", array_values_count=" << array_values_count << ", intern_string_count=" << intern_string_count << ", resolve_string_count=" << resolve_string_count << ", resolve_method_type_count=" << resolve_method_type_count; } } void Transaction::Abort(const std::string& abort_message) { // We may abort more than once if the exception thrown at the time of the // previous abort has been caught during execution of a class initializer. // We just keep the message of the first abort because it will cause the // transaction to be rolled back anyway. if (!aborted_) { aborted_ = true; abort_message_ = abort_message; } } void Transaction::ThrowAbortError(Thread* self, const std::string* abort_message) { const bool rethrow = (abort_message == nullptr); if (kIsDebugBuild && rethrow) { CHECK(IsAborted()) << "Rethrow " << DescriptorToDot(Transaction::kAbortExceptionDescriptor) << " while transaction is not aborted"; } if (rethrow) { // Rethrow an exception with the earlier abort message stored in the transaction. self->ThrowNewWrappedException(Transaction::kAbortExceptionDescriptor, GetAbortMessage().c_str()); } else { // Throw an exception with the given abort message. self->ThrowNewWrappedException(Transaction::kAbortExceptionDescriptor, abort_message->c_str()); } } const std::string& Transaction::GetAbortMessage() const { return abort_message_; } bool Transaction::WriteConstraint(ObjPtr obj) const { DCHECK(obj != nullptr); // Prevent changes in boot image spaces for app or boot image extension. // For boot image there are no boot image spaces and this condition evaluates to false. if (heap_->ObjectIsInBootImageSpace(obj)) { return true; } // For apps, also prevent writing to other classes. return IsStrict() && obj->IsClass() && // no constraint updating instances or arrays obj != root_; // modifying other classes' static field, fail } bool Transaction::WriteValueConstraint(ObjPtr value) const { if (value == nullptr) { return false; // We can always store null values. } gc::Heap* heap = Runtime::Current()->GetHeap(); if (IsStrict()) { // TODO: Should we restrict writes the same way as for boot image extension? return false; } else if (heap->GetBootImageSpaces().empty()) { return false; // No constraints for boot image. } else { // Boot image extension. ObjPtr klass = value->IsClass() ? value->AsClass() : value->GetClass(); return !AotClassLinker::CanReferenceInBootImageExtension(klass, heap); } } bool Transaction::ReadConstraint(ObjPtr obj) const { // Read constraints are checked only for static field reads as there are // no constraints on reading instance fields and array elements. DCHECK(obj->IsClass()); if (IsStrict()) { return obj != root_; // fail if not self-updating } else { // For boot image and boot image extension, allow reading any field. return false; } } inline Transaction::ObjectLog& Transaction::GetOrCreateObjectLog(mirror::Object* obj) { return object_logs_.GetOrCreate(obj, [&]() { return ObjectLog(&allocator_); }); } void Transaction::RecordWriteFieldBoolean(mirror::Object* obj, MemberOffset field_offset, uint8_t value, 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); } void Transaction::RecordWriteFieldByte(mirror::Object* obj, MemberOffset field_offset, int8_t value, 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); } void Transaction::RecordWriteFieldChar(mirror::Object* obj, MemberOffset field_offset, uint16_t value, 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); } void Transaction::RecordWriteFieldShort(mirror::Object* obj, MemberOffset field_offset, int16_t value, 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); } void Transaction::RecordWriteField32(mirror::Object* obj, MemberOffset field_offset, uint32_t value, 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); } void Transaction::RecordWriteField64(mirror::Object* obj, MemberOffset field_offset, uint64_t value, 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); } void Transaction::RecordWriteFieldReference(mirror::Object* obj, MemberOffset field_offset, mirror::Object* value, 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); } void Transaction::RecordWriteArray(mirror::Array* array, size_t index, uint64_t value) { DCHECK(array != nullptr); 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); } void Transaction::RecordResolveString(ObjPtr dex_cache, dex::StringIndex string_idx) { DCHECK(dex_cache != nullptr); DCHECK_LT(string_idx.index_, dex_cache->GetDexFile()->NumStringIds()); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; resolve_string_logs_.emplace_front(dex_cache, string_idx); } void Transaction::RecordResolveMethodType(ObjPtr dex_cache, dex::ProtoIndex proto_idx) { DCHECK(dex_cache != nullptr); DCHECK_LT(proto_idx.index_, dex_cache->GetDexFile()->NumProtoIds()); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; resolve_method_type_logs_.emplace_front(dex_cache, proto_idx); } void Transaction::RecordStrongStringInsertion(ObjPtr s) { InternStringLog log(s, InternStringLog::kStrongString, InternStringLog::kInsert); LogInternedString(std::move(log)); } void Transaction::RecordWeakStringInsertion(ObjPtr s) { InternStringLog log(s, InternStringLog::kWeakString, InternStringLog::kInsert); LogInternedString(std::move(log)); } void Transaction::RecordStrongStringRemoval(ObjPtr s) { InternStringLog log(s, InternStringLog::kStrongString, InternStringLog::kRemove); LogInternedString(std::move(log)); } void Transaction::RecordWeakStringRemoval(ObjPtr s) { InternStringLog log(s, InternStringLog::kWeakString, InternStringLog::kRemove); LogInternedString(std::move(log)); } void Transaction::LogInternedString(InternStringLog&& log) { Locks::intern_table_lock_->AssertExclusiveHeld(Thread::Current()); DCHECK(assert_no_new_records_reason_ == nullptr) << assert_no_new_records_reason_; intern_string_logs_.push_front(std::move(log)); } void Transaction::Rollback() { Thread* self = Thread::Current(); self->AssertNoPendingException(); MutexLock mu(self, *Locks::intern_table_lock_); rolling_back_ = true; CHECK(!Runtime::Current()->IsActiveTransaction()); UndoObjectModifications(); UndoArrayModifications(); UndoInternStringTableModifications(); UndoResolveStringModifications(); UndoResolveMethodTypeModifications(); rolling_back_ = false; } void Transaction::UndoObjectModifications() { // TODO we may not need to restore objects allocated during this transaction. Or we could directly // remove them from the heap. for (const auto& it : object_logs_) { it.second.Undo(it.first); } object_logs_.clear(); } void Transaction::UndoArrayModifications() { // TODO we may not need to restore array allocated during this transaction. Or we could directly // remove them from the heap. for (const auto& it : array_logs_) { it.second.Undo(it.first); } array_logs_.clear(); } void Transaction::UndoInternStringTableModifications() { InternTable* const intern_table = Runtime::Current()->GetInternTable(); // We want to undo each operation from the most recent to the oldest. List has been filled so the // most recent operation is at list begin so just have to iterate over it. for (const InternStringLog& string_log : intern_string_logs_) { string_log.Undo(intern_table); } intern_string_logs_.clear(); } void Transaction::UndoResolveStringModifications() { for (ResolveStringLog& string_log : resolve_string_logs_) { string_log.Undo(); } resolve_string_logs_.clear(); } void Transaction::UndoResolveMethodTypeModifications() { for (ResolveMethodTypeLog& method_type_log : resolve_method_type_logs_) { method_type_log.Undo(); } resolve_method_type_logs_.clear(); } void Transaction::VisitRoots(RootVisitor* visitor) { // Transactions are used for single-threaded initialization. // This is the only function that should be called from a different thread, // namely the GC thread, and it is called with the mutator lock held exclusively, // so the data structures in the `Transaction` are protected from concurrent use. DCHECK(Locks::mutator_lock_->IsExclusiveHeld(Thread::Current())); visitor->VisitRoot(reinterpret_cast(&root_), RootInfo(kRootUnknown)); { // Create a separate `ArenaStack` for this thread. ArenaStack arena_stack(Runtime::Current()->GetArenaPool()); VisitObjectLogs(visitor, &arena_stack); VisitArrayLogs(visitor, &arena_stack); } VisitInternStringLogs(visitor); VisitResolveStringLogs(visitor); VisitResolveMethodTypeLogs(visitor); } template void UpdateKeys(const MovingRoots& moving_roots, Container& container) { for (const auto& pair : moving_roots) { auto* old_root = pair.first; auto* new_root = pair.second; auto node = container.extract(old_root); CHECK(!node.empty()); node.key() = new_root; bool inserted = container.insert(std::move(node)).inserted; CHECK(inserted); } } void Transaction::VisitObjectLogs(RootVisitor* visitor, ArenaStack* arena_stack) { // List of moving roots. ScopedArenaAllocator allocator(arena_stack); using ObjectPair = std::pair; ScopedArenaForwardList moving_roots(allocator.Adapter(kArenaAllocTransaction)); // Visit roots. for (auto& it : object_logs_) { it.second.VisitRoots(visitor); mirror::Object* old_root = it.first; mirror::Object* new_root = old_root; visitor->VisitRoot(&new_root, RootInfo(kRootUnknown)); if (new_root != old_root) { moving_roots.push_front(std::make_pair(old_root, new_root)); } } // Update object logs with moving roots. UpdateKeys(moving_roots, object_logs_); } void Transaction::VisitArrayLogs(RootVisitor* visitor, ArenaStack* arena_stack) { // List of moving roots. ScopedArenaAllocator allocator(arena_stack); using ArrayPair = std::pair; ScopedArenaForwardList moving_roots(allocator.Adapter(kArenaAllocTransaction)); for (auto& it : array_logs_) { mirror::Array* old_root = it.first; mirror::Array* new_root = old_root; visitor->VisitRoot(reinterpret_cast(&new_root), RootInfo(kRootUnknown)); if (new_root != old_root) { moving_roots.push_front(std::make_pair(old_root, new_root)); } } // Update array logs with moving roots. UpdateKeys(moving_roots, array_logs_); } void Transaction::VisitInternStringLogs(RootVisitor* visitor) { for (InternStringLog& log : intern_string_logs_) { log.VisitRoots(visitor); } } void Transaction::VisitResolveStringLogs(RootVisitor* visitor) { for (ResolveStringLog& log : resolve_string_logs_) { log.VisitRoots(visitor); } } void Transaction::VisitResolveMethodTypeLogs(RootVisitor* visitor) { for (ResolveMethodTypeLog& log : resolve_method_type_logs_) { log.VisitRoots(visitor); } } void Transaction::ObjectLog::LogBooleanValue(MemberOffset offset, uint8_t value, bool is_volatile) { LogValue(ObjectLog::kBoolean, offset, value, is_volatile); } void Transaction::ObjectLog::LogByteValue(MemberOffset offset, int8_t value, bool is_volatile) { LogValue(ObjectLog::kByte, offset, value, is_volatile); } void Transaction::ObjectLog::LogCharValue(MemberOffset offset, uint16_t value, bool is_volatile) { LogValue(ObjectLog::kChar, offset, value, is_volatile); } void Transaction::ObjectLog::LogShortValue(MemberOffset offset, int16_t value, bool is_volatile) { LogValue(ObjectLog::kShort, offset, value, is_volatile); } void Transaction::ObjectLog::Log32BitsValue(MemberOffset offset, uint32_t value, bool is_volatile) { LogValue(ObjectLog::k32Bits, offset, value, is_volatile); } void Transaction::ObjectLog::Log64BitsValue(MemberOffset offset, uint64_t value, bool is_volatile) { LogValue(ObjectLog::k64Bits, offset, value, is_volatile); } void Transaction::ObjectLog::LogReferenceValue(MemberOffset offset, mirror::Object* obj, bool is_volatile) { LogValue(ObjectLog::kReference, offset, reinterpret_cast(obj), is_volatile); } void Transaction::ObjectLog::LogValue(ObjectLog::FieldValueKind kind, MemberOffset offset, uint64_t value, bool is_volatile) { auto it = field_values_.find(offset.Uint32Value()); if (it == field_values_.end()) { ObjectLog::FieldValue field_value; field_value.value = value; field_value.is_volatile = is_volatile; field_value.kind = kind; field_values_.emplace(offset.Uint32Value(), std::move(field_value)); } } void Transaction::ObjectLog::Undo(mirror::Object* obj) const { for (auto& it : field_values_) { // Garbage collector needs to access object's class and array's length. So we don't rollback // these values. MemberOffset field_offset(it.first); if (field_offset.Uint32Value() == mirror::Class::ClassOffset().Uint32Value()) { // Skip Object::class field. continue; } if (obj->IsArrayInstance() && field_offset.Uint32Value() == mirror::Array::LengthOffset().Uint32Value()) { // Skip Array::length field. continue; } const FieldValue& field_value = it.second; UndoFieldWrite(obj, field_offset, field_value); } } void Transaction::ObjectLog::UndoFieldWrite(mirror::Object* obj, MemberOffset field_offset, const FieldValue& field_value) const { // TODO We may want to abort a transaction while still being in transaction mode. In this case, // we'd need to disable the check. constexpr bool kCheckTransaction = false; switch (field_value.kind) { case kBoolean: if (UNLIKELY(field_value.is_volatile)) { obj->SetFieldBooleanVolatile( field_offset, field_value.value); } else { obj->SetFieldBoolean( field_offset, field_value.value); } break; case kByte: if (UNLIKELY(field_value.is_volatile)) { obj->SetFieldByteVolatile( field_offset, static_cast(field_value.value)); } else { obj->SetFieldByte( field_offset, static_cast(field_value.value)); } break; case kChar: if (UNLIKELY(field_value.is_volatile)) { obj->SetFieldCharVolatile( field_offset, static_cast(field_value.value)); } else { obj->SetFieldChar( field_offset, static_cast(field_value.value)); } break; case kShort: if (UNLIKELY(field_value.is_volatile)) { obj->SetFieldShortVolatile( field_offset, static_cast(field_value.value)); } else { obj->SetFieldShort( field_offset, static_cast(field_value.value)); } break; case k32Bits: if (UNLIKELY(field_value.is_volatile)) { obj->SetField32Volatile( field_offset, static_cast(field_value.value)); } else { obj->SetField32( field_offset, static_cast(field_value.value)); } break; case k64Bits: if (UNLIKELY(field_value.is_volatile)) { obj->SetField64Volatile(field_offset, field_value.value); } else { obj->SetField64(field_offset, field_value.value); } break; case kReference: if (UNLIKELY(field_value.is_volatile)) { obj->SetFieldObjectVolatile( field_offset, reinterpret_cast(field_value.value)); } else { obj->SetFieldObject( field_offset, reinterpret_cast(field_value.value)); } break; default: LOG(FATAL) << "Unknown value kind " << static_cast(field_value.kind); UNREACHABLE(); } } void Transaction::ObjectLog::VisitRoots(RootVisitor* visitor) { for (auto& it : field_values_) { FieldValue& field_value = it.second; if (field_value.kind == ObjectLog::kReference) { visitor->VisitRootIfNonNull(reinterpret_cast(&field_value.value), RootInfo(kRootUnknown)); } } } void Transaction::InternStringLog::Undo(InternTable* intern_table) const { DCHECK(!Runtime::Current()->IsActiveTransaction()); DCHECK(intern_table != nullptr); ObjPtr s = str_.Read(); uint32_t hash = static_cast(s->GetStoredHashCode()); switch (string_op_) { case InternStringLog::kInsert: { switch (string_kind_) { case InternStringLog::kStrongString: intern_table->RemoveStrong(s, hash); break; case InternStringLog::kWeakString: intern_table->RemoveWeak(s, hash); break; default: LOG(FATAL) << "Unknown interned string kind"; UNREACHABLE(); } break; } case InternStringLog::kRemove: { switch (string_kind_) { case InternStringLog::kStrongString: intern_table->InsertStrong(s, hash); break; case InternStringLog::kWeakString: intern_table->InsertWeak(s, hash); break; default: LOG(FATAL) << "Unknown interned string kind"; UNREACHABLE(); } break; } default: LOG(FATAL) << "Unknown interned string op"; UNREACHABLE(); } } void Transaction::InternStringLog::VisitRoots(RootVisitor* visitor) { str_.VisitRoot(visitor, RootInfo(kRootInternedString)); } void Transaction::ResolveStringLog::Undo() const { dex_cache_.Read()->ClearString(string_idx_); } Transaction::ResolveStringLog::ResolveStringLog(ObjPtr dex_cache, dex::StringIndex string_idx) : dex_cache_(dex_cache), string_idx_(string_idx) { DCHECK(dex_cache != nullptr); DCHECK_LT(string_idx_.index_, dex_cache->GetDexFile()->NumStringIds()); } void Transaction::ResolveStringLog::VisitRoots(RootVisitor* visitor) { dex_cache_.VisitRoot(visitor, RootInfo(kRootVMInternal)); } void Transaction::ResolveMethodTypeLog::Undo() const { dex_cache_.Read()->ClearMethodType(proto_idx_); } Transaction::ResolveMethodTypeLog::ResolveMethodTypeLog(ObjPtr dex_cache, dex::ProtoIndex proto_idx) : dex_cache_(dex_cache), proto_idx_(proto_idx) { DCHECK(dex_cache != nullptr); DCHECK_LT(proto_idx_.index_, dex_cache->GetDexFile()->NumProtoIds()); } void Transaction::ResolveMethodTypeLog::VisitRoots(RootVisitor* visitor) { dex_cache_.VisitRoot(visitor, RootInfo(kRootVMInternal)); } Transaction::InternStringLog::InternStringLog(ObjPtr s, StringKind kind, StringOp op) : str_(s), string_kind_(kind), string_op_(op) { DCHECK(s != nullptr); } void Transaction::ArrayLog::LogValue(size_t index, uint64_t value) { // Add a mapping if there is none yet. array_values_.FindOrAdd(index, value); } void Transaction::ArrayLog::Undo(mirror::Array* array) const { DCHECK(array != nullptr); DCHECK(array->IsArrayInstance()); Primitive::Type type = array->GetClass()->GetComponentType()->GetPrimitiveType(); for (auto it : array_values_) { UndoArrayWrite(array, type, it.first, it.second); } } void Transaction::ArrayLog::UndoArrayWrite(mirror::Array* array, Primitive::Type array_type, size_t index, uint64_t value) const { // TODO We may want to abort a transaction while still being in transaction mode. In this case, // we'd need to disable the check. constexpr bool kCheckTransaction = false; switch (array_type) { case Primitive::kPrimBoolean: array->AsBooleanArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimByte: array->AsByteArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimChar: array->AsCharArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimShort: array->AsShortArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimInt: array->AsIntArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimFloat: array->AsFloatArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimLong: array->AsLongArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimDouble: array->AsDoubleArray()->SetWithoutChecks( index, static_cast(value)); break; case Primitive::kPrimNot: LOG(FATAL) << "ObjectArray should be treated as Object"; UNREACHABLE(); default: LOG(FATAL) << "Unsupported type " << array_type; UNREACHABLE(); } } Transaction* ScopedAssertNoNewTransactionRecords::InstallAssertion(const char* reason) { Transaction* transaction = nullptr; if (kIsDebugBuild && Runtime::Current()->IsActiveTransaction()) { transaction = Runtime::Current()->GetTransaction(); if (transaction != nullptr) { CHECK(transaction->assert_no_new_records_reason_ == nullptr) << "old: " << transaction->assert_no_new_records_reason_ << " new: " << reason; transaction->assert_no_new_records_reason_ = reason; } } return transaction; } void ScopedAssertNoNewTransactionRecords::RemoveAssertion(Transaction* transaction) { if (kIsDebugBuild) { CHECK(Runtime::Current()->GetTransaction() == transaction); CHECK(transaction->assert_no_new_records_reason_ != nullptr); transaction->assert_no_new_records_reason_ = nullptr; } } } // namespace art