| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/webcodecs/reclaimable_codec.h" |
| |
| #include "base/location.h" |
| #include "base/time/default_tick_clock.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/dom/quota_exceeded_error.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/modules/webcodecs/codec_pressure_manager.h" |
| #include "third_party/blink/renderer/modules/webcodecs/codec_pressure_manager_provider.h" |
| #include "third_party/blink/renderer/platform/heap/persistent.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| constexpr base::TimeDelta ReclaimableCodec::kInactivityReclamationThreshold; |
| |
| ReclaimableCodec::ReclaimableCodec(CodecType type, ExecutionContext* context) |
| : ExecutionContextLifecycleObserver(context), |
| codec_type_(type), |
| tick_clock_(base::DefaultTickClock::GetInstance()), |
| inactivity_threshold_(kInactivityReclamationThreshold), |
| last_activity_(tick_clock_->NowTicks()), |
| activity_timer_(context->GetTaskRunner(TaskType::kInternalMedia), |
| this, |
| &ReclaimableCodec::OnActivityTimerFired) { |
| DCHECK(context); |
| // Do this last, it will immediately re-enter via OnLifecycleStateChanged(). |
| observer_handle_ = context->GetScheduler()->AddLifecycleObserver( |
| FrameOrWorkerScheduler::ObserverType::kWorkerScheduler, |
| BindRepeating(&ReclaimableCodec::OnLifecycleStateChanged, |
| WrapWeakPersistent(this))); |
| } |
| |
| void ReclaimableCodec::Trace(Visitor* visitor) const { |
| visitor->Trace(activity_timer_); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| } |
| |
| void ReclaimableCodec::ApplyCodecPressure() { |
| if (is_applying_pressure_) |
| return; |
| |
| is_applying_pressure_ = true; |
| |
| if (auto* pressure_manager = PressureManager()) |
| pressure_manager->AddCodec(this); |
| } |
| |
| void ReclaimableCodec::ReleaseCodecPressure() { |
| if (!is_applying_pressure_) { |
| DCHECK(!activity_timer_.IsActive()); |
| return; |
| } |
| |
| if (auto* pressure_manager = PressureManager()) { |
| // If we fail to get |pressure_manager| here (say, because the |
| // ExecutionContext is being destroyed), this is harmless. The |
| // CodecPressureManager maintains its own local pressure count, and it will |
| // properly decrement it from the global pressure count upon the manager's |
| // disposal. The CodecPressureManager's WeakMember reference to |this| will |
| // be cleared by the GC when |this| is disposed. The manager might still |
| // call into SetGlobalPressureExceededFlag() before |this| is disposed, but |
| // we will simply noop those calls. |
| pressure_manager->RemoveCodec(this); |
| } |
| |
| // We might still exceed global codec pressure at this point, but this codec |
| // isn't contributing to it, and needs to reset its own flag. |
| SetGlobalPressureExceededFlag(false); |
| |
| is_applying_pressure_ = false; |
| } |
| |
| void ReclaimableCodec::Dispose() { |
| if (!is_applying_pressure_) |
| return; |
| |
| if (auto* pressure_manager = PressureManager()) |
| pressure_manager->OnCodecDisposed(this); |
| } |
| |
| void ReclaimableCodec::SetGlobalPressureExceededFlag( |
| bool global_pressure_exceeded) { |
| if (!is_applying_pressure_) { |
| // We should only hit this call because we failed to get the |
| // PressureManager() in ReleaseCodecPressure(). See the note above. |
| DCHECK(!PressureManager()); |
| return; |
| } |
| |
| if (global_pressure_exceeded_ == global_pressure_exceeded) |
| return; |
| |
| global_pressure_exceeded_ = global_pressure_exceeded; |
| |
| OnReclamationPreconditionsUpdated(); |
| } |
| |
| void ReclaimableCodec::OnLifecycleStateChanged( |
| scheduler::SchedulingLifecycleState lifecycle_state) { |
| DVLOG(5) << __func__ |
| << " lifecycle_state=" << static_cast<int>(lifecycle_state); |
| bool is_backgrounded = |
| lifecycle_state != scheduler::SchedulingLifecycleState::kNotThrottled; |
| |
| // Several life cycle states map to "backgrounded", but we only want to |
| // observe the transition. |
| if (is_backgrounded == is_backgrounded_) |
| return; |
| |
| is_backgrounded_ = is_backgrounded; |
| |
| // Make sure we wait the full inactivity timer period before reclaiming a |
| // newly backgrounded codec. |
| if (is_backgrounded_) |
| MarkCodecActive(); |
| |
| OnReclamationPreconditionsUpdated(); |
| } |
| |
| void ReclaimableCodec::SimulateLifecycleStateForTesting( |
| scheduler::SchedulingLifecycleState state) { |
| OnLifecycleStateChanged(state); |
| } |
| |
| void ReclaimableCodec::SimulateCodecReclaimedForTesting() { |
| auto* message = "Codec reclaimed for testing."; |
| if (RuntimeEnabledFeatures::QuotaExceededErrorUpdateEnabled()) { |
| OnCodecReclaimed(MakeGarbageCollected<QuotaExceededError>(message)); |
| } else { |
| OnCodecReclaimed(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kQuotaExceededError, message)); |
| } |
| } |
| |
| void ReclaimableCodec::SimulateActivityTimerFiredForTesting() { |
| OnActivityTimerFired(nullptr); |
| } |
| |
| void ReclaimableCodec::MarkCodecActive() { |
| last_activity_ = tick_clock_->NowTicks(); |
| last_tick_was_inactive_ = false; |
| } |
| |
| void ReclaimableCodec::OnReclamationPreconditionsUpdated() { |
| if (AreReclamationPreconditionsMet()) |
| StartIdleReclamationTimer(); |
| else |
| StopIdleReclamationTimer(); |
| } |
| |
| bool ReclaimableCodec::AreReclamationPreconditionsMet() { |
| // If |global_pressure_exceeded_| is true, so should |is_applying_pressure_|. |
| DCHECK_EQ(global_pressure_exceeded_, |
| global_pressure_exceeded_ && is_applying_pressure_); |
| |
| return is_applying_pressure_ && global_pressure_exceeded_ && is_backgrounded_; |
| } |
| |
| void ReclaimableCodec::StartIdleReclamationTimer() { |
| DCHECK(AreReclamationPreconditionsMet()); |
| |
| if (activity_timer_.IsActive()) |
| return; |
| |
| DVLOG(5) << __func__ << " Starting timer."; |
| activity_timer_.StartRepeating(inactivity_threshold_ / 2, FROM_HERE); |
| } |
| |
| void ReclaimableCodec::StopIdleReclamationTimer() { |
| DCHECK(!AreReclamationPreconditionsMet()); |
| |
| activity_timer_.Stop(); |
| } |
| |
| void ReclaimableCodec::OnActivityTimerFired(TimerBase*) { |
| DCHECK(AreReclamationPreconditionsMet()); |
| |
| auto time_inactive = tick_clock_->NowTicks() - last_activity_; |
| bool is_inactive = time_inactive >= inactivity_threshold_; |
| |
| // Do not immediately reclaim. Make sure the codec is inactive for 2 ticks. |
| // Otherwise, tabs that were suspended could see their codecs reclaimed |
| // immediately after being resumed. |
| if (is_inactive && last_tick_was_inactive_) { |
| activity_timer_.Stop(); |
| auto* message = "Codec reclaimed due to inactivity."; |
| if (RuntimeEnabledFeatures::QuotaExceededErrorUpdateEnabled()) { |
| OnCodecReclaimed(MakeGarbageCollected<QuotaExceededError>(message)); |
| } else { |
| OnCodecReclaimed(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kQuotaExceededError, message)); |
| } |
| } |
| |
| last_tick_was_inactive_ = time_inactive >= (inactivity_threshold_ / 2); |
| } |
| |
| CodecPressureManager* ReclaimableCodec::PressureManager() { |
| auto* execution_context = GetExecutionContext(); |
| |
| if (!execution_context || execution_context->IsContextDestroyed()) |
| return nullptr; |
| |
| auto& manager_provider = |
| CodecPressureManagerProvider::From(*execution_context); |
| |
| switch (codec_type_) { |
| case CodecType::kDecoder: |
| return manager_provider.GetDecoderPressureManager(); |
| case CodecType::kEncoder: |
| return manager_provider.GetEncoderPressureManager(); |
| } |
| } |
| |
| } // namespace blink |