| // 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/editing/finder/find_task_controller.h" |
| |
| #include "third_party/blink/public/mojom/frame/find_in_page.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h" |
| #include "third_party/blink/renderer/core/display_lock/display_lock_document_state.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/range.h" |
| #include "third_party/blink/renderer/core/dom/scripted_idle_task_controller.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/finder/find_buffer.h" |
| #include "third_party/blink/renderer/core/editing/finder/find_options.h" |
| #include "third_party/blink/renderer/core/editing/finder/text_finder.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/instrumentation/histogram.h" |
| |
| namespace blink { |
| |
| namespace { |
| constexpr base::TimeDelta kFindTaskTimeAllotment = |
| base::TimeDelta::FromMilliseconds(10); |
| } // namespace |
| |
| class FindTaskController::FindTask final : public GarbageCollected<FindTask> { |
| public: |
| FindTask(FindTaskController* controller, |
| Document* document, |
| int identifier, |
| const WebString& search_text, |
| const mojom::blink::FindOptions& options) |
| : document_(document), |
| controller_(controller), |
| identifier_(identifier), |
| search_text_(search_text), |
| options_(options.Clone()) { |
| DCHECK(document_); |
| if (options.run_synchronously_for_testing) { |
| Invoke(); |
| } else { |
| controller_->GetLocalFrame() |
| ->GetTaskRunner(blink::TaskType::kInternalFindInPage) |
| ->PostTask(FROM_HERE, |
| WTF::Bind(&FindTask::Invoke, WrapWeakPersistent(this))); |
| } |
| } |
| |
| void Trace(Visitor* visitor) const { |
| visitor->Trace(controller_); |
| visitor->Trace(document_); |
| } |
| |
| void Invoke() { |
| const base::TimeTicks task_start_time = base::TimeTicks::Now(); |
| if (!controller_) |
| return; |
| if (!controller_->ShouldFindMatches(identifier_, search_text_, *options_)) { |
| controller_->DidFinishTask(identifier_, search_text_, *options_, |
| true /* finished_whole_request */, |
| PositionInFlatTree(), 0 /* match_count */, |
| true /* aborted */, task_start_time); |
| return; |
| } |
| SCOPED_UMA_HISTOGRAM_TIMER("WebCore.FindInPage.TaskDuration"); |
| |
| Document* document = controller_->GetLocalFrame()->GetDocument(); |
| if (!document || document_ != document) |
| return; |
| auto forced_activatable_display_locks = |
| document->GetDisplayLockDocumentState() |
| .GetScopedForceActivatableLocks(); |
| PositionInFlatTree search_start = |
| PositionInFlatTree::FirstPositionInNode(*document); |
| PositionInFlatTree search_end; |
| if (document->documentElement() && |
| document->documentElement()->lastChild()) { |
| search_end = PositionInFlatTree::AfterNode( |
| *document->documentElement()->lastChild()); |
| } else { |
| search_end = PositionInFlatTree::LastPositionInNode(*document); |
| } |
| DCHECK_EQ(search_start.GetDocument(), search_end.GetDocument()); |
| |
| if (Range* resume_from_range = controller_->ResumeFindingFromRange()) { |
| // This is a continuation of a finding operation that timed out and didn't |
| // complete last time around, so we should start from where we left off. |
| DCHECK(resume_from_range->collapsed()); |
| search_start = FromPositionInDOMTree<EditingInFlatTreeStrategy>( |
| resume_from_range->EndPosition()); |
| if (search_start.GetDocument() != search_end.GetDocument()) |
| return; |
| } |
| |
| // This is required if we forced any of the display-locks. |
| document->UpdateStyleAndLayout(DocumentUpdateReason::kFindInPage); |
| |
| int match_count = 0; |
| bool full_range_searched = false; |
| PositionInFlatTree next_task_start_position; |
| |
| blink::FindOptions find_options = |
| (options_->forward ? 0 : kBackwards) | |
| (options_->match_case ? 0 : kCaseInsensitive) | |
| (options_->new_session ? kStartInSelection : 0); |
| auto start_time = base::TimeTicks::Now(); |
| |
| while (search_start < search_end) { |
| // Find in the whole block. |
| FindBuffer buffer(EphemeralRangeInFlatTree(search_start, search_end)); |
| FindBuffer::Results match_results = |
| buffer.FindMatches(search_text_, find_options); |
| for (FindBuffer::BufferMatchResult match : match_results) { |
| const EphemeralRangeInFlatTree ephemeral_match_range = |
| buffer.RangeFromBufferIndex(match.start, |
| match.start + match.length); |
| auto* const match_range = MakeGarbageCollected<Range>( |
| ephemeral_match_range.GetDocument(), |
| ToPositionInDOMTree(ephemeral_match_range.StartPosition()), |
| ToPositionInDOMTree(ephemeral_match_range.EndPosition())); |
| if (match_range->collapsed()) { |
| // resultRange will be collapsed if the matched text spans over |
| // multiple TreeScopes. TODO(rakina): Show such matches to users. |
| next_task_start_position = ephemeral_match_range.EndPosition(); |
| continue; |
| } |
| ++match_count; |
| controller_->DidFindMatch(identifier_, match_range); |
| } |
| // At this point, all text in the block collected above has been |
| // processed. Now we move to the next block if there's any, |
| // otherwise we should stop. |
| search_start = buffer.PositionAfterBlock(); |
| if (search_start.IsNull()) { |
| full_range_searched = true; |
| break; |
| } |
| next_task_start_position = search_start; |
| auto time_elapsed = base::TimeTicks::Now() - start_time; |
| if (time_elapsed > kFindTaskTimeAllotment) |
| break; |
| } |
| controller_->DidFinishTask(identifier_, search_text_, *options_, |
| full_range_searched, next_task_start_position, |
| match_count, false /* aborted */, |
| task_start_time); |
| } |
| |
| Member<Document> document_; |
| Member<FindTaskController> controller_; |
| const int identifier_; |
| const WebString search_text_; |
| mojom::blink::FindOptionsPtr options_; |
| }; |
| |
| FindTaskController::FindTaskController(WebLocalFrameImpl& owner_frame, |
| TextFinder& text_finder) |
| : owner_frame_(owner_frame), |
| text_finder_(text_finder), |
| resume_finding_from_range_(nullptr) {} |
| |
| void FindTaskController::StartRequest( |
| int identifier, |
| const WebString& search_text, |
| const mojom::blink::FindOptions& options) { |
| TRACE_EVENT_ASYNC_BEGIN0("blink", "FindInPageRequest", identifier); |
| current_request_start_time_ = base::TimeTicks::Now(); |
| total_task_duration_for_current_request_ = base::TimeDelta(); |
| task_count_for_current_request_ = 0; |
| DCHECK(!finding_in_progress_); |
| DCHECK_EQ(current_find_identifier_, kInvalidFindIdentifier); |
| // This is a brand new search, so we need to reset everything. |
| finding_in_progress_ = true; |
| current_match_count_ = 0; |
| current_find_identifier_ = identifier; |
| RequestFindTask(identifier, search_text, options); |
| } |
| |
| void FindTaskController::CancelPendingRequest() { |
| if (find_task_) |
| find_task_.Clear(); |
| if (finding_in_progress_) { |
| RecordRequestMetrics(RequestEndState::ABORTED); |
| last_find_request_completed_with_no_matches_ = false; |
| } |
| finding_in_progress_ = false; |
| resume_finding_from_range_ = nullptr; |
| current_find_identifier_ = kInvalidFindIdentifier; |
| } |
| |
| void FindTaskController::RequestFindTask( |
| int identifier, |
| const WebString& search_text, |
| const mojom::blink::FindOptions& options) { |
| DCHECK_EQ(find_task_, nullptr); |
| DCHECK_EQ(identifier, current_find_identifier_); |
| task_count_for_current_request_++; |
| find_task_ = MakeGarbageCollected<FindTask>( |
| this, GetLocalFrame()->GetDocument(), identifier, search_text, options); |
| } |
| |
| void FindTaskController::DidFinishTask( |
| int identifier, |
| const WebString& search_text, |
| const mojom::blink::FindOptions& options, |
| bool finished_whole_request, |
| PositionInFlatTree next_starting_position, |
| int match_count, |
| bool aborted, |
| base::TimeTicks task_start_time) { |
| if (current_find_identifier_ != identifier) |
| return; |
| total_task_duration_for_current_request_ += |
| base::TimeTicks::Now() - task_start_time; |
| if (find_task_) |
| find_task_.Clear(); |
| // Remember what we search for last time, so we can skip searching if more |
| // letters are added to the search string (and last outcome was 0). |
| last_search_string_ = search_text; |
| |
| if (next_starting_position.IsNotNull()) { |
| resume_finding_from_range_ = MakeGarbageCollected<Range>( |
| *next_starting_position.GetDocument(), |
| ToPositionInDOMTree(next_starting_position), |
| ToPositionInDOMTree(next_starting_position)); |
| } |
| |
| if (match_count > 0) { |
| text_finder_->UpdateMatches(identifier, match_count, |
| finished_whole_request); |
| } |
| |
| if (!finished_whole_request) { |
| // Task ran out of time, request for another one. |
| RequestFindTask(identifier, search_text, options); |
| return; // Done for now, resume work later. |
| } |
| |
| text_finder_->FinishCurrentScopingEffort(identifier); |
| |
| RecordRequestMetrics(RequestEndState::ABORTED); |
| last_find_request_completed_with_no_matches_ = |
| !aborted && !current_match_count_; |
| finding_in_progress_ = false; |
| current_find_identifier_ = kInvalidFindIdentifier; |
| } |
| |
| void FindTaskController::RecordRequestMetrics( |
| RequestEndState request_end_state) { |
| bool aborted = (request_end_state == RequestEndState::ABORTED); |
| TRACE_EVENT_ASYNC_END1("blink", "FindInPageRequest", current_find_identifier_, |
| "aborted", aborted); |
| if (aborted) { |
| UMA_HISTOGRAM_MEDIUM_TIMES("WebCore.FindInPage.TotalTaskDuration.Aborted", |
| total_task_duration_for_current_request_); |
| UMA_HISTOGRAM_MEDIUM_TIMES( |
| "WebCore.FindInPage.RequestDuration.Aborted", |
| base::TimeTicks::Now() - current_request_start_time_); |
| UMA_HISTOGRAM_COUNTS_1000( |
| "WebCore.FindInPage.NumberOfTasksPerRequest.Aborted", |
| task_count_for_current_request_); |
| } else { |
| UMA_HISTOGRAM_MEDIUM_TIMES("WebCore.FindInPage.TotalTaskDuration.Finished", |
| total_task_duration_for_current_request_); |
| UMA_HISTOGRAM_MEDIUM_TIMES( |
| "WebCore.FindInPage.RequestDuration.Finished", |
| base::TimeTicks::Now() - current_request_start_time_); |
| UMA_HISTOGRAM_COUNTS_1000( |
| "WebCore.FindInPage.NumberOfTasksPerRequest.Finished", |
| task_count_for_current_request_); |
| } |
| } |
| |
| LocalFrame* FindTaskController::GetLocalFrame() const { |
| return OwnerFrame().GetFrame(); |
| } |
| |
| bool FindTaskController::ShouldFindMatches( |
| int identifier, |
| const String& search_text, |
| const mojom::blink::FindOptions& options) { |
| if (identifier != current_find_identifier_) |
| return false; |
| // Don't scope if we can't find a frame, a document, or a view. |
| // The user may have closed the tab/application, so abort. |
| LocalFrame* frame = GetLocalFrame(); |
| if (!frame || !frame->View() || !frame->GetPage() || !frame->GetDocument()) |
| return false; |
| |
| DCHECK(frame->GetDocument()); |
| DCHECK(frame->View()); |
| |
| if (options.force) |
| return true; |
| |
| if (!OwnerFrame().HasVisibleContent()) |
| return false; |
| |
| // If the frame completed the scoping operation and found 0 matches the last |
| // time it was searched, then we don't have to search it again if the user is |
| // just adding to the search string or sending the same search string again. |
| if (last_find_request_completed_with_no_matches_ && |
| !last_search_string_.IsEmpty()) { |
| // Check to see if the search string prefixes match. |
| String previous_search_prefix = |
| search_text.Substring(0, last_search_string_.length()); |
| |
| if (previous_search_prefix == last_search_string_) |
| return false; // Don't search this frame, it will be fruitless. |
| } |
| |
| return true; |
| } |
| |
| void FindTaskController::DidFindMatch(int identifier, Range* result_range) { |
| current_match_count_++; |
| text_finder_->DidFindMatch(identifier, current_match_count_, result_range); |
| } |
| |
| void FindTaskController::Trace(Visitor* visitor) const { |
| visitor->Trace(owner_frame_); |
| visitor->Trace(text_finder_); |
| visitor->Trace(find_task_); |
| visitor->Trace(resume_finding_from_range_); |
| } |
| |
| void FindTaskController::ResetLastFindRequestCompletedWithNoMatches() { |
| last_find_request_completed_with_no_matches_ = false; |
| } |
| |
| } // namespace blink |