| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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/core/display_lock/display_lock_context.h" |
| |
| #include "base/memory/ptr_util.h" |
| #include "third_party/blink/renderer/core/display_lock/display_lock_options.h" |
| #include "third_party/blink/renderer/core/display_lock/strict_yielding_display_lock_budget.h" |
| #include "third_party/blink/renderer/core/display_lock/unyielding_display_lock_budget.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/node_computed_style.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_trace_events.h" |
| #include "third_party/blink/renderer/core/layout/layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| #include "third_party/blink/renderer/platform/bindings/microtask.h" |
| |
| namespace blink { |
| |
| namespace { |
| // The default timeout for the lock if a timeout is not specified. Defaults to 1 |
| // sec. |
| double kDefaultLockTimeoutMs = 1000.; |
| |
| // Helper function that resolves the given promise. Used to delay a resolution |
| // to be in a task queue. |
| void ResolvePromise(ScriptPromiseResolver* resolver) { |
| resolver->Resolve(); |
| } |
| |
| // Helper function that returns an immediately rejected promise. |
| ScriptPromise GetRejectedPromise(ScriptState* script_state) { |
| auto* resolver = ScriptPromiseResolver::Create(script_state); |
| auto promise = resolver->Promise(); |
| resolver->Reject(); |
| return promise; |
| } |
| |
| // Helper function that returns an immediately resolved promise. |
| ScriptPromise GetResolvedPromise(ScriptState* script_state) { |
| auto* resolver = ScriptPromiseResolver::Create(script_state); |
| auto promise = resolver->Promise(); |
| resolver->Resolve(); |
| return promise; |
| } |
| |
| } // namespace |
| |
| DisplayLockContext::DisplayLockContext(Element* element, |
| ExecutionContext* context) |
| : ContextLifecycleObserver(context), |
| element_(element), |
| weak_factory_(this) { |
| DCHECK(element_->GetDocument().View()); |
| element_->GetDocument().View()->RegisterForLifecycleNotifications(this); |
| } |
| |
| DisplayLockContext::~DisplayLockContext() { |
| DCHECK_EQ(state_, kUnlocked); |
| } |
| |
| void DisplayLockContext::Trace(blink::Visitor* visitor) { |
| visitor->Trace(update_resolver_); |
| visitor->Trace(commit_resolver_); |
| visitor->Trace(element_); |
| ScriptWrappable::Trace(visitor); |
| ActiveScriptWrappable::Trace(visitor); |
| ContextLifecycleObserver::Trace(visitor); |
| } |
| |
| void DisplayLockContext::Dispose() { |
| // Note that if we have any resolvers at dispose time, then it's too late to |
| // reject the promise, since we are not allowed to create new strong |
| // references to objects already set for destruction (and rejecting would do |
| // this since the rejection has to be deferred). We need to detach instead. |
| // TODO(vmpstr): See if there is another earlier time we can detect that we're |
| // going to be disposed. |
| FinishUpdateResolver(kDetach); |
| FinishCommitResolver(kDetach); |
| state_ = kUnlocked; |
| |
| if (element_ && element_->GetDocument().View()) |
| element_->GetDocument().View()->UnregisterFromLifecycleNotifications(this); |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| void DisplayLockContext::ContextDestroyed(ExecutionContext*) { |
| FinishUpdateResolver(kReject); |
| FinishCommitResolver(kReject); |
| state_ = kUnlocked; |
| } |
| |
| bool DisplayLockContext::HasPendingActivity() const { |
| // If we're locked or doing any work and have an element, then we should stay |
| // alive. If the element is gone, then there is no reason for the context to |
| // remain. Also, if we're unlocked we're essentially "idle" so GC can clean us |
| // up. If the script needs the context, the element would create a new one. |
| return element_ && state_ != kUnlocked; |
| } |
| |
| ScriptPromise DisplayLockContext::acquire(ScriptState* script_state, |
| DisplayLockOptions* options) { |
| // TODO(vmpstr): We don't support locking connected elements for now. |
| if (element_->isConnected()) |
| return GetRejectedPromise(script_state); |
| |
| double timeout_ms = (options && options->hasTimeout()) |
| ? options->timeout() |
| : kDefaultLockTimeoutMs; |
| // We always reschedule a timeout task even if we're not starting a new |
| // acquire. The reason for this is that the last acquire dictates the timeout |
| // interval. Note that the following call cancels any existing timeout tasks. |
| RescheduleTimeoutTask(timeout_ms); |
| |
| // We must already be locked if we're not unlocked. |
| if (state_ != kUnlocked) |
| return GetResolvedPromise(script_state); |
| |
| // TODO(vmpstr): This will always currently result in an empty layout rect, |
| // but when we handle connected elements, this will capture the current frame |
| // rect. |
| if (!locked_frame_rect_) { |
| auto* layout_object = element_->GetLayoutObject(); |
| if (layout_object && layout_object->IsBox()) { |
| locked_frame_rect_ = ToLayoutBox(layout_object)->FrameRect(); |
| } else { |
| locked_frame_rect_ = LayoutRect(); |
| } |
| } |
| |
| // Since we're not connected at this point, we can lock immediately. |
| state_ = kLocked; |
| update_budget_.reset(); |
| return GetResolvedPromise(script_state); |
| } |
| |
| ScriptPromise DisplayLockContext::update(ScriptState* script_state) { |
| // Reject if we're unlocked. |
| if (state_ == kUnlocked) |
| return GetRejectedPromise(script_state); |
| |
| // If we have a resolver, then we're at least updating already, just return |
| // the same promise. |
| if (update_resolver_) { |
| DCHECK(state_ == kUpdating || state_ == kCommitting) << state_; |
| return update_resolver_->Promise(); |
| } |
| |
| update_resolver_ = ScriptPromiseResolver::Create(script_state); |
| // We only need to kick off an Update if we're in a locked state, all other |
| // states are already updating. |
| if (state_ == kLocked) |
| StartUpdate(); |
| return update_resolver_->Promise(); |
| } |
| |
| ScriptPromise DisplayLockContext::commit(ScriptState* script_state) { |
| // Reject if we're unlocked. |
| if (state_ == kUnlocked) |
| return GetRejectedPromise(script_state); |
| |
| // If we have a resolver, we must be committing already, just return the same |
| // promise. |
| if (commit_resolver_) { |
| DCHECK(state_ == kCommitting) << state_; |
| return commit_resolver_->Promise(); |
| } |
| |
| // Now that we've explicitly been requested to commit, we have cancel the |
| // timeout task. |
| CancelTimeoutTask(); |
| |
| // Note that we don't resolve the update promise here, since it should still |
| // finish updating before resolution. That is, calling update() and commit() |
| // together will still wait until the lifecycle is clean before resolving any |
| // of the promises. |
| DCHECK_NE(state_, kCommitting); |
| commit_resolver_ = ScriptPromiseResolver::Create(script_state); |
| StartCommit(); |
| return commit_resolver_->Promise(); |
| } |
| |
| void DisplayLockContext::FinishUpdateResolver(ResolverState state) { |
| if (!update_resolver_) |
| return; |
| switch (state) { |
| case kResolve: |
| // In order to avoid script doing work as a part of the lifecycle update, |
| // we delay the resolution to be in a task. |
| GetExecutionContext() |
| ->GetTaskRunner(TaskType::kMiscPlatformAPI) |
| ->PostTask(FROM_HERE, |
| WTF::Bind(&ResolvePromise, |
| WrapPersistent(update_resolver_.Get()))); |
| break; |
| case kReject: |
| update_resolver_->Reject(); |
| break; |
| case kDetach: |
| update_resolver_->Detach(); |
| } |
| update_resolver_ = nullptr; |
| } |
| |
| void DisplayLockContext::FinishCommitResolver(ResolverState state) { |
| if (!commit_resolver_) |
| return; |
| switch (state) { |
| case kResolve: |
| // In order to avoid script doing work as a part of the lifecycle update, |
| // we delay the resolution to be in a task. |
| GetExecutionContext() |
| ->GetTaskRunner(TaskType::kMiscPlatformAPI) |
| ->PostTask(FROM_HERE, |
| WTF::Bind(&ResolvePromise, |
| WrapPersistent(commit_resolver_.Get()))); |
| break; |
| case kReject: |
| commit_resolver_->Reject(); |
| break; |
| case kDetach: |
| commit_resolver_->Detach(); |
| } |
| commit_resolver_ = nullptr; |
| } |
| |
| bool DisplayLockContext::ShouldStyle() const { |
| return update_forced_ || state_ > kUpdating || |
| (state_ == kUpdating && |
| update_budget_->ShouldPerformPhase(DisplayLockBudget::Phase::kStyle)); |
| } |
| |
| void DisplayLockContext::DidStyle() { |
| if (state_ != kCommitting && state_ != kUpdating && !update_forced_) |
| return; |
| |
| // We must have contain: content for display locking. |
| // Note that we should also have content containment even if we're forcing |
| // this update to happen. Otherwise, proceeding with layout may cause |
| // unexpected behavior. By rejecting the promise, the behavior can be detected |
| // by script. |
| auto* style = element_->GetComputedStyle(); |
| if (!style || !style->ContainsContent()) { |
| FinishUpdateResolver(kReject); |
| FinishCommitResolver(kReject); |
| state_ = state_ == kUpdating ? kLocked : kUnlocked; |
| return; |
| } |
| |
| if (state_ == kUpdating) |
| update_budget_->DidPerformPhase(DisplayLockBudget::Phase::kStyle); |
| } |
| |
| bool DisplayLockContext::ShouldLayout() const { |
| return update_forced_ || state_ > kUpdating || |
| (state_ == kUpdating && update_budget_->ShouldPerformPhase( |
| DisplayLockBudget::Phase::kLayout)); |
| } |
| |
| void DisplayLockContext::DidLayout() { |
| if (state_ == kUpdating) |
| update_budget_->DidPerformPhase(DisplayLockBudget::Phase::kLayout); |
| } |
| |
| bool DisplayLockContext::ShouldPrePaint() const { |
| return update_forced_ || state_ > kUpdating || |
| (state_ == kUpdating && update_budget_->ShouldPerformPhase( |
| DisplayLockBudget::Phase::kPrePaint)); |
| } |
| |
| void DisplayLockContext::DidPrePaint() { |
| if (state_ == kUpdating) |
| update_budget_->DidPerformPhase(DisplayLockBudget::Phase::kPrePaint); |
| |
| #if DCHECK_IS_ON() |
| if (state_ == kUpdating || state_ == kCommitting) { |
| // Since we should be under containment, we should have a layer. If we |
| // don't, then paint might not happen and we'll never resolve. |
| DCHECK(element_->GetLayoutObject()->HasLayer()); |
| } |
| #endif |
| } |
| |
| bool DisplayLockContext::ShouldPaint() const { |
| // Note that forced updates should never require us to paint, so we don't |
| // check |update_forced_| here. In other words, although |update_forced_| |
| // could be true here, we still should not paint. This also holds for |
| // kUpdating state, since updates should not paint. |
| return state_ >= kCommitting; |
| } |
| |
| void DisplayLockContext::DidPaint() { |
| // This is here for symmetry, but could be removed if necessary. |
| } |
| |
| void DisplayLockContext::DidAttachLayoutTree() { |
| if (state_ == kUnlocked) |
| return; |
| |
| // Note that although we checked at style recalc time that the element has |
| // "contain: content", it might not actually apply the containment (e.g. see |
| // ShouldApplyContentContainment()). This confirms that containment should |
| // apply. |
| auto* layout_object = element_->GetLayoutObject(); |
| if (!layout_object || !layout_object->ShouldApplyContentContainment()) { |
| FinishUpdateResolver(kReject); |
| FinishCommitResolver(kReject); |
| state_ = state_ == kUpdating ? kLocked : kUnlocked; |
| } |
| } |
| |
| DisplayLockContext::ScopedPendingFrameRect |
| DisplayLockContext::GetScopedPendingFrameRect() { |
| if (state_ >= kCommitting) |
| return ScopedPendingFrameRect(nullptr); |
| |
| DCHECK(element_->GetLayoutObject() && element_->GetLayoutBox()); |
| element_->GetLayoutBox()->SetFrameRectForDisplayLock(pending_frame_rect_); |
| return ScopedPendingFrameRect(this); |
| } |
| |
| void DisplayLockContext::NotifyPendingFrameRectScopeEnded() { |
| DCHECK(element_->GetLayoutObject() && element_->GetLayoutBox()); |
| DCHECK(locked_frame_rect_); |
| pending_frame_rect_ = element_->GetLayoutBox()->FrameRect(); |
| element_->GetLayoutBox()->SetFrameRectForDisplayLock(*locked_frame_rect_); |
| } |
| |
| DisplayLockContext::ScopedForcedUpdate |
| DisplayLockContext::GetScopedForcedUpdate() { |
| if (state_ >= kCommitting) |
| return ScopedForcedUpdate(nullptr); |
| |
| DCHECK(!update_forced_); |
| update_forced_ = true; |
| |
| // Now that the update is forced, we should ensure that style layout, and |
| // prepaint code can reach it via dirty bits. Note that paint isn't a part of |
| // this, since |update_forced_| doesn't force paint to happen. See |
| // ShouldPaint(). |
| MarkAncestorsForStyleRecalcIfNeeded(); |
| MarkAncestorsForLayoutIfNeeded(); |
| MarkAncestorsForPrePaintIfNeeded(); |
| return ScopedForcedUpdate(this); |
| } |
| |
| void DisplayLockContext::NotifyForcedUpdateScopeEnded() { |
| DCHECK(update_forced_); |
| update_forced_ = false; |
| } |
| |
| void DisplayLockContext::StartCommit() { |
| DCHECK_LT(state_, kCommitting); |
| if (state_ != kUpdating) |
| ScheduleAnimation(); |
| state_ = kCommitting; |
| update_budget_.reset(); |
| |
| // We're committing without a budget, so ensure we can reach style. |
| MarkAncestorsForStyleRecalcIfNeeded(); |
| |
| auto* layout_object = element_->GetLayoutObject(); |
| // We might commit without connecting, so there is no layout object yet. |
| if (!layout_object) |
| return; |
| |
| // Now that we know we have a layout object, we should ensure that we can |
| // reach the rest of the phases as well. |
| MarkAncestorsForLayoutIfNeeded(); |
| MarkAncestorsForPrePaintIfNeeded(); |
| MarkPaintLayerNeedsRepaint(); |
| |
| // We also need to commit the pending frame rect at this point. |
| bool frame_rect_changed = |
| ToLayoutBox(layout_object)->FrameRect() != pending_frame_rect_; |
| |
| // If the frame rect hasn't actually changed then we don't need to do |
| // anything. Other than wait for commit to happen |
| if (!frame_rect_changed) |
| return; |
| |
| // Set the pending frame rect as the new one, and ensure to schedule a layout |
| // for just the box itself. Note that we use the non-display locked version to |
| // ensure all the hooks are property invoked. |
| ToLayoutBox(layout_object)->SetFrameRect(pending_frame_rect_); |
| layout_object->SetNeedsLayout( |
| layout_invalidation_reason::kDisplayLockCommitting); |
| } |
| |
| void DisplayLockContext::StartUpdate() { |
| DCHECK_EQ(state_, kLocked); |
| state_ = kUpdating; |
| // We don't need to mark anything dirty since the budget will take care of |
| // that for us. |
| update_budget_ = CreateNewBudget(); |
| ScheduleAnimation(); |
| } |
| |
| std::unique_ptr<DisplayLockBudget> DisplayLockContext::CreateNewBudget() { |
| switch (BudgetType::kDefault) { |
| case BudgetType::kDoNotYield: |
| return base::WrapUnique(new UnyieldingDisplayLockBudget(this)); |
| case BudgetType::kStrictYieldBetweenLifecyclePhases: |
| return base::WrapUnique(new StrictYieldingDisplayLockBudget(this)); |
| case BudgetType::kYieldBetweenLifecyclePhases: |
| NOTIMPLEMENTED(); |
| return nullptr; |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| bool DisplayLockContext::MarkAncestorsForStyleRecalcIfNeeded() { |
| if (IsElementDirtyForStyleRecalc()) { |
| element_->MarkAncestorsWithChildNeedsStyleRecalc(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool DisplayLockContext::MarkAncestorsForLayoutIfNeeded() { |
| if (IsElementDirtyForLayout()) { |
| element_->GetLayoutObject()->MarkContainerChainForLayout(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool DisplayLockContext::MarkAncestorsForPrePaintIfNeeded() { |
| if (IsElementDirtyForPrePaint()) { |
| auto* layout_object = element_->GetLayoutObject(); |
| if (auto* parent = layout_object->Parent()) |
| parent->SetSubtreeShouldCheckForPaintInvalidation(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool DisplayLockContext::MarkPaintLayerNeedsRepaint() { |
| if (auto* layout_object = element_->GetLayoutObject()) { |
| layout_object->PaintingLayer()->SetNeedsRepaint(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool DisplayLockContext::IsElementDirtyForStyleRecalc() const { |
| return element_->NeedsStyleRecalc() || element_->ChildNeedsStyleRecalc(); |
| } |
| |
| bool DisplayLockContext::IsElementDirtyForLayout() const { |
| if (auto* layout_object = element_->GetLayoutObject()) |
| return layout_object->NeedsLayout(); |
| return false; |
| } |
| |
| bool DisplayLockContext::IsElementDirtyForPrePaint() const { |
| if (auto* layout_object = element_->GetLayoutObject()) { |
| return layout_object->ShouldCheckForPaintInvalidation() || |
| layout_object->SubtreeShouldCheckForPaintInvalidation() || |
| layout_object->NeedsPaintPropertyUpdate() || |
| layout_object->DescendantNeedsPaintPropertyUpdate(); |
| } |
| return false; |
| } |
| |
| void DisplayLockContext::DidMoveToNewDocument(Document& old_document) { |
| // Since we're observing the lifecycle updates, ensure that we listen to the |
| // right document's view. |
| if (old_document.View()) |
| old_document.View()->UnregisterFromLifecycleNotifications(this); |
| if (element_ && element_->GetDocument().View()) |
| element_->GetDocument().View()->RegisterForLifecycleNotifications(this); |
| } |
| |
| void DisplayLockContext::WillStartLifecycleUpdate() { |
| if (state_ == kUpdating) |
| update_budget_->WillStartLifecycleUpdate(); |
| } |
| |
| void DisplayLockContext::DidFinishLifecycleUpdate() { |
| if (state_ == kCommitting) { |
| FinishUpdateResolver(kResolve); |
| FinishCommitResolver(kResolve); |
| state_ = kUnlocked; |
| return; |
| } |
| |
| if (state_ != kUpdating) |
| return; |
| |
| if (update_budget_->NeedsLifecycleUpdates()) { |
| // Note that we post a task to schedule an animation, since rAF requests can |
| // be ignored if they happen from within a lifecycle update. |
| GetExecutionContext() |
| ->GetTaskRunner(TaskType::kMiscPlatformAPI) |
| ->PostTask(FROM_HERE, WTF::Bind(&DisplayLockContext::ScheduleAnimation, |
| WrapWeakPersistent(this))); |
| return; |
| } |
| |
| FinishUpdateResolver(kResolve); |
| update_budget_.reset(); |
| state_ = kLocked; |
| } |
| |
| void DisplayLockContext::ScheduleAnimation() { |
| // Schedule an animation to perform the lifecycle phases. |
| element_->GetDocument().GetPage()->Animator().ScheduleVisualUpdate( |
| element_->GetDocument().GetFrame()); |
| } |
| |
| void DisplayLockContext::RescheduleTimeoutTask(double delay) { |
| CancelTimeoutTask(); |
| |
| if (!std::isfinite(delay)) |
| return; |
| |
| // Make sure the delay is at least 1ms. |
| delay = std::max(delay, 1.); |
| GetExecutionContext() |
| ->GetTaskRunner(TaskType::kMiscPlatformAPI) |
| ->PostDelayedTask(FROM_HERE, |
| WTF::Bind(&DisplayLockContext::TriggerTimeout, |
| weak_factory_.GetWeakPtr()), |
| TimeDelta::FromMillisecondsD(delay)); |
| timeout_task_is_scheduled_ = true; |
| } |
| |
| void DisplayLockContext::CancelTimeoutTask() { |
| if (!timeout_task_is_scheduled_) |
| return; |
| weak_factory_.InvalidateWeakPtrs(); |
| timeout_task_is_scheduled_ = false; |
| } |
| |
| void DisplayLockContext::TriggerTimeout() { |
| if (element_ && state_ < kCommitting) |
| StartCommit(); |
| timeout_task_is_scheduled_ = false; |
| } |
| |
| // Scoped objects implementation |
| // ----------------------------------------------- |
| |
| DisplayLockContext::ScopedPendingFrameRect::ScopedPendingFrameRect( |
| DisplayLockContext* context) |
| : context_(context) {} |
| |
| DisplayLockContext::ScopedPendingFrameRect::ScopedPendingFrameRect( |
| ScopedPendingFrameRect&& other) |
| : context_(other.context_) { |
| other.context_ = nullptr; |
| } |
| |
| DisplayLockContext::ScopedPendingFrameRect::~ScopedPendingFrameRect() { |
| if (context_) |
| context_->NotifyPendingFrameRectScopeEnded(); |
| } |
| |
| DisplayLockContext::ScopedForcedUpdate::ScopedForcedUpdate( |
| DisplayLockContext* context) |
| : context_(context) {} |
| |
| DisplayLockContext::ScopedForcedUpdate::ScopedForcedUpdate( |
| ScopedForcedUpdate&& other) |
| : context_(other.context_) { |
| other.context_ = nullptr; |
| } |
| |
| DisplayLockContext::ScopedForcedUpdate::~ScopedForcedUpdate() { |
| if (context_) |
| context_->NotifyForcedUpdateScopeEnded(); |
| } |
| |
| } // namespace blink |