blob: fb00b62a386a87a40a2f9c5494803237c262d73f [file] [log] [blame]
/*
* Copyright (C) 2010 Google Inc. All rights reserved.
* Copyright (C) 2012 Intel Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/timing/performance.h"
#include <algorithm>
#include "base/metrics/histogram_macros.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "third_party/blink/public/mojom/feature_policy/document_policy_feature.mojom-blink.h"
#include "third_party/blink/public/mojom/timing/worker_timing_container.mojom-blink-forward.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/string_or_performance_measure_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_object_builder.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_performance_mark_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_performance_measure_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_profiler_init_options.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_timing.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/loader/document_load_timing.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/timing/largest_contentful_paint.h"
#include "third_party/blink/renderer/core/timing/layout_shift.h"
#include "third_party/blink/renderer/core/timing/measure_memory/measure_memory_controller.h"
#include "third_party/blink/renderer/core/timing/performance_element_timing.h"
#include "third_party/blink/renderer/core/timing/performance_event_timing.h"
#include "third_party/blink/renderer/core/timing/performance_long_task_timing.h"
#include "third_party/blink/renderer/core/timing/performance_mark.h"
#include "third_party/blink/renderer/core/timing/performance_measure.h"
#include "third_party/blink/renderer/core/timing/performance_observer.h"
#include "third_party/blink/renderer/core/timing/performance_resource_timing.h"
#include "third_party/blink/renderer/core/timing/performance_user_timing.h"
#include "third_party/blink/renderer/core/timing/profiler.h"
#include "third_party/blink/renderer/core/timing/profiler_group.h"
#include "third_party/blink/renderer/core/timing/time_clamper.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_load_timing.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_timing_info.h"
#include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
namespace blink {
namespace {
const SecurityOrigin* GetSecurityOrigin(ExecutionContext* context) {
if (context)
return context->GetSecurityOrigin();
return nullptr;
}
bool IsMeasureOptionsEmpty(const PerformanceMeasureOptions& options) {
return !options.hasDetail() && !options.hasEnd() && !options.hasStart() &&
!options.hasDuration();
}
} // namespace
using PerformanceObserverVector = HeapVector<Member<PerformanceObserver>>;
constexpr size_t kDefaultResourceTimingBufferSize = 250;
constexpr size_t kDefaultEventTimingBufferSize = 150;
constexpr size_t kDefaultElementTimingBufferSize = 150;
constexpr size_t kDefaultLayoutShiftBufferSize = 150;
constexpr size_t kDefaultLargestContenfulPaintSize = 150;
constexpr size_t kDefaultLongTaskBufferSize = 200;
Performance::Performance(
base::TimeTicks time_origin,
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: resource_timing_buffer_size_limit_(kDefaultResourceTimingBufferSize),
event_timing_buffer_max_size_(kDefaultEventTimingBufferSize),
element_timing_buffer_max_size_(kDefaultElementTimingBufferSize),
user_timing_(nullptr),
time_origin_(time_origin),
tick_clock_(base::DefaultTickClock::GetInstance()),
observer_filter_options_(PerformanceEntry::kInvalid),
task_runner_(std::move(task_runner)),
deliver_observations_timer_(task_runner_,
this,
&Performance::DeliverObservationsTimerFired),
resource_timing_buffer_full_timer_(
task_runner_,
this,
&Performance::FireResourceTimingBufferFull) {
unix_at_zero_monotonic_ = ConvertSecondsToDOMHighResTimeStamp(
base::DefaultClock::GetInstance()->Now().ToDoubleT() -
tick_clock_->NowTicks().since_origin().InSecondsF());
}
Performance::~Performance() = default;
const AtomicString& Performance::InterfaceName() const {
return event_target_names::kPerformance;
}
PerformanceTiming* Performance::timing() const {
return nullptr;
}
PerformanceNavigation* Performance::navigation() const {
return nullptr;
}
MemoryInfo* Performance::memory() const {
return nullptr;
}
EventCounts* Performance::eventCounts() {
return nullptr;
}
ScriptPromise Performance::measureUserAgentSpecificMemory(
ScriptState* script_state,
ExceptionState& exception_state) const {
return MeasureMemoryController::StartMeasurement(script_state,
exception_state);
}
DOMHighResTimeStamp Performance::timeOrigin() const {
DCHECK(!time_origin_.is_null());
return unix_at_zero_monotonic_ +
ConvertTimeTicksToDOMHighResTimeStamp(time_origin_);
}
PerformanceEntryVector Performance::getEntries() {
PerformanceEntryVector entries;
entries.AppendVector(resource_timing_buffer_);
if (first_input_timing_)
entries.push_back(first_input_timing_);
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
// This extra checking is needed when WorkerPerformance
// calls this method.
if (navigation_timing_)
entries.push_back(navigation_timing_);
if (user_timing_) {
entries.AppendVector(user_timing_->GetMarks());
entries.AppendVector(user_timing_->GetMeasures());
}
if (first_paint_timing_)
entries.push_back(first_paint_timing_);
if (first_contentful_paint_timing_)
entries.push_back(first_contentful_paint_timing_);
std::sort(entries.begin(), entries.end(),
PerformanceEntry::StartTimeCompareLessThan);
return entries;
}
PerformanceEntryVector Performance::getBufferedEntriesByType(
const AtomicString& entry_type) {
PerformanceEntry::EntryType type =
PerformanceEntry::ToEntryTypeEnum(entry_type);
return getEntriesByTypeInternal(type);
}
PerformanceEntryVector Performance::getEntriesByType(
const AtomicString& entry_type) {
PerformanceEntry::EntryType type =
PerformanceEntry::ToEntryTypeEnum(entry_type);
if (!PerformanceEntry::IsValidTimelineEntryType(type)) {
PerformanceEntryVector empty_entries;
String message = "Deprecated API for given entry type.";
GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning, message));
return empty_entries;
}
return getEntriesByTypeInternal(type);
}
PerformanceEntryVector Performance::getEntriesByTypeInternal(
PerformanceEntry::EntryType type) {
PerformanceEntryVector entries;
switch (type) {
case PerformanceEntry::kResource:
UseCounter::Count(GetExecutionContext(), WebFeature::kResourceTiming);
for (const auto& resource : resource_timing_buffer_)
entries.push_back(resource);
break;
case PerformanceEntry::kElement:
for (const auto& element : element_timing_buffer_)
entries.push_back(element);
break;
case PerformanceEntry::kEvent:
UseCounter::Count(GetExecutionContext(),
WebFeature::kEventTimingExplicitlyRequested);
for (const auto& event : event_timing_buffer_)
entries.push_back(event);
break;
case PerformanceEntry::kFirstInput:
UseCounter::Count(GetExecutionContext(),
WebFeature::kEventTimingExplicitlyRequested);
UseCounter::Count(GetExecutionContext(),
WebFeature::kEventTimingFirstInputExplicitlyRequested);
if (first_input_timing_)
entries.push_back(first_input_timing_);
break;
case PerformanceEntry::kNavigation:
UseCounter::Count(GetExecutionContext(), WebFeature::kNavigationTimingL2);
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
if (navigation_timing_)
entries.push_back(navigation_timing_);
break;
case PerformanceEntry::kMark:
if (user_timing_)
entries.AppendVector(user_timing_->GetMarks());
break;
case PerformanceEntry::kMeasure:
if (user_timing_)
entries.AppendVector(user_timing_->GetMeasures());
break;
case PerformanceEntry::kPaint:
UseCounter::Count(GetExecutionContext(),
WebFeature::kPaintTimingRequested);
if (first_paint_timing_)
entries.push_back(first_paint_timing_);
if (first_contentful_paint_timing_)
entries.push_back(first_contentful_paint_timing_);
break;
case PerformanceEntry::kLongTask:
for (const auto& entry : longtask_buffer_)
entries.push_back(entry);
break;
// TaskAttribution entries are only associated to longtask entries.
case PerformanceEntry::kTaskAttribution:
break;
case PerformanceEntry::kLayoutShift:
for (const auto& layout_shift : layout_shift_buffer_)
entries.push_back(layout_shift);
break;
case PerformanceEntry::kLargestContentfulPaint:
entries.AppendVector(largest_contentful_paint_buffer_);
break;
case PerformanceEntry::kVisibilityState:
entries.AppendVector(visibility_state_buffer_);
break;
case PerformanceEntry::kInvalid:
break;
}
std::sort(entries.begin(), entries.end(),
PerformanceEntry::StartTimeCompareLessThan);
return entries;
}
PerformanceEntryVector Performance::getEntriesByName(
const AtomicString& name,
const AtomicString& entry_type) {
PerformanceEntryVector entries;
PerformanceEntry::EntryType type =
PerformanceEntry::ToEntryTypeEnum(entry_type);
if (!entry_type.IsNull() &&
!PerformanceEntry::IsValidTimelineEntryType(type)) {
String message = "Deprecated API for given entry type.";
GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning, message));
return entries;
}
if (entry_type.IsNull() || type == PerformanceEntry::kResource) {
for (const auto& resource : resource_timing_buffer_) {
if (resource->name() == name)
entries.push_back(resource);
}
}
if (entry_type.IsNull() || type == PerformanceEntry::kFirstInput) {
if (first_input_timing_ && first_input_timing_->name() == name)
entries.push_back(first_input_timing_);
}
if (type == PerformanceEntry::kFirstInput) {
UseCounter::Count(GetExecutionContext(),
WebFeature::kEventTimingExplicitlyRequested);
}
if (entry_type.IsNull() || type == PerformanceEntry::kNavigation) {
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
if (navigation_timing_ && navigation_timing_->name() == name)
entries.push_back(navigation_timing_);
}
if (user_timing_) {
if (entry_type.IsNull() || type == PerformanceEntry::kMark)
entries.AppendVector(user_timing_->GetMarks(name));
if (entry_type.IsNull() || type == PerformanceEntry::kMeasure)
entries.AppendVector(user_timing_->GetMeasures(name));
}
if (entry_type.IsNull() || type == PerformanceEntry::kPaint) {
if (first_paint_timing_ && first_paint_timing_->name() == name)
entries.push_back(first_paint_timing_);
if (first_contentful_paint_timing_ &&
first_contentful_paint_timing_->name() == name)
entries.push_back(first_contentful_paint_timing_);
}
std::sort(entries.begin(), entries.end(),
PerformanceEntry::StartTimeCompareLessThan);
return entries;
}
void Performance::clearResourceTimings() {
resource_timing_buffer_.clear();
}
void Performance::setResourceTimingBufferSize(unsigned size) {
resource_timing_buffer_size_limit_ = size;
}
bool Performance::PassesTimingAllowCheck(
const ResourceResponse& response,
const ResourceResponse& next_response,
const SecurityOrigin& initiator_security_origin,
ExecutionContext* context,
bool* response_tainting_not_basic,
bool* tainted_origin_flag) {
DCHECK(response_tainting_not_basic);
DCHECK(tainted_origin_flag);
const KURL& response_url = response.ResponseUrl();
scoped_refptr<const SecurityOrigin> resource_origin =
SecurityOrigin::Create(response_url);
bool is_same_origin =
resource_origin->IsSameOriginWith(&initiator_security_origin);
if (!*response_tainting_not_basic && is_same_origin)
return true;
*response_tainting_not_basic = true;
const AtomicString& timing_allow_origin_string =
response.HttpHeaderField(http_names::kTimingAllowOrigin);
if (timing_allow_origin_string.IsEmpty())
return false;
const String& security_origin = initiator_security_origin.ToString();
CommaDelimitedHeaderSet tao_headers;
ParseCommaDelimitedHeader(timing_allow_origin_string, tao_headers);
if (tao_headers.size() == 1u) {
if (*tao_headers.begin() == "*") {
UseCounter::Count(context, WebFeature::kStarInTimingAllowOrigin);
return true;
} else {
UseCounter::Count(context, WebFeature::kSingleOriginInTimingAllowOrigin);
}
} else if (tao_headers.size() > 1u) {
UseCounter::Count(context, WebFeature::kMultipleOriginsInTimingAllowOrigin);
}
bool is_next_resource_same_origin = true;
// Only do the origin check if |next_response| is not equal to |response|.
if (&next_response != &response) {
is_next_resource_same_origin =
SecurityOrigin::Create(next_response.ResponseUrl())
->IsSameOriginWith(resource_origin.get());
}
if (!is_same_origin && !is_next_resource_same_origin)
*tainted_origin_flag = true;
bool contains_security_origin = false;
for (const String& header : tao_headers) {
if (header == "*")
return true;
if (header == security_origin)
contains_security_origin = true;
}
// If the tainted origin flag is set and the header contains the origin, this
// means that this method currently passes the check but once we implement the
// tainted origin flag properly then it will fail the check. Record this in a
// UseCounter to track how many webpages contain resources where the new check
// would fail.
if (*tainted_origin_flag && contains_security_origin) {
UseCounter::Count(context,
WebFeature::kResourceTimingTaintedOriginFlagFail);
}
return contains_security_origin;
}
bool Performance::AllowsTimingRedirect(
const Vector<ResourceResponse>& redirect_chain,
const ResourceResponse& final_response,
const SecurityOrigin& initiator_security_origin,
ExecutionContext* context) {
bool response_tainting_not_basic = false;
bool tainted_origin_flag = false;
for (unsigned i = 0; i < redirect_chain.size(); ++i) {
const ResourceResponse& response = redirect_chain[i];
const ResourceResponse& next_response =
i + 1 < redirect_chain.size() ? redirect_chain[i + 1] : final_response;
if (!PassesTimingAllowCheck(
response, next_response, initiator_security_origin, context,
&response_tainting_not_basic, &tainted_origin_flag))
return false;
}
if (!PassesTimingAllowCheck(
final_response, final_response, initiator_security_origin, context,
&response_tainting_not_basic, &tainted_origin_flag)) {
return false;
}
return true;
}
void Performance::GenerateAndAddResourceTiming(
const ResourceTimingInfo& info,
const AtomicString& initiator_type) {
ExecutionContext* context = GetExecutionContext();
const SecurityOrigin* security_origin = GetSecurityOrigin(context);
if (!security_origin)
return;
// |info| is taken const-ref but this can make destructive changes to
// WorkerTimingContainer on |info| when a page is controlled by a service
// worker.
AddResourceTiming(
GenerateResourceTiming(*security_origin, info, *context),
!initiator_type.IsNull() ? initiator_type : info.InitiatorType(),
info.TakeWorkerTimingReceiver(), context);
}
mojom::blink::ResourceTimingInfoPtr Performance::GenerateResourceTiming(
const SecurityOrigin& destination_origin,
const ResourceTimingInfo& info,
ExecutionContext& context_for_use_counter) {
// TODO(dcheng): It would be nicer if the performance entries simply held this
// data internally, rather than requiring it be marshalled back and forth.
const ResourceResponse& final_response = info.FinalResponse();
mojom::blink::ResourceTimingInfoPtr result =
mojom::blink::ResourceTimingInfo::New();
result->name = info.InitialURL().GetString();
result->start_time = info.InitialTime();
result->alpn_negotiated_protocol =
final_response.AlpnNegotiatedProtocol().IsNull()
? g_empty_string
: final_response.AlpnNegotiatedProtocol();
result->connection_info = final_response.ConnectionInfoString().IsNull()
? g_empty_string
: final_response.ConnectionInfoString();
result->timing = final_response.GetResourceLoadTiming()
? final_response.GetResourceLoadTiming()->ToMojo()
: nullptr;
result->response_end = info.LoadResponseEnd();
result->context_type = info.ContextType();
result->request_destination = info.RequestDestination();
bool response_tainting_not_basic = false;
bool tainted_origin_flag = false;
result->allow_timing_details = PassesTimingAllowCheck(
final_response, final_response, destination_origin,
&context_for_use_counter, &response_tainting_not_basic,
&tainted_origin_flag);
const Vector<ResourceResponse>& redirect_chain = info.RedirectChain();
if (!redirect_chain.IsEmpty()) {
result->allow_redirect_details =
AllowsTimingRedirect(redirect_chain, final_response, destination_origin,
&context_for_use_counter);
// TODO(https://crbug.com/817691): is |last_chained_timing| being null a bug
// or is this if statement reasonable?
if (ResourceLoadTiming* last_chained_timing =
redirect_chain.back().GetResourceLoadTiming()) {
result->last_redirect_end_time = last_chained_timing->ReceiveHeadersEnd();
} else {
result->allow_redirect_details = false;
result->last_redirect_end_time = base::TimeTicks();
}
if (!result->allow_redirect_details) {
// TODO(https://crbug.com/817691): There was previously a DCHECK that
// |final_timing| is non-null. However, it clearly can be null: removing
// this check caused https://crbug.com/803811. Figure out how this can
// happen so test coverage can be added.
if (ResourceLoadTiming* final_timing =
final_response.GetResourceLoadTiming()) {
result->start_time = final_timing->RequestTime();
}
}
} else {
result->allow_redirect_details = false;
result->last_redirect_end_time = base::TimeTicks();
}
result->transfer_size = info.TransferSize();
result->encoded_body_size = final_response.EncodedBodyLength();
result->decoded_body_size = final_response.DecodedBodyLength();
result->did_reuse_connection = final_response.ConnectionReused();
// TODO(crbug.com/1153336) Use network::IsUrlPotentiallyTrustworthy().
result->is_secure_context =
SecurityOrigin::IsSecure(final_response.ResponseUrl());
result->allow_negative_values = info.NegativeAllowed();
if (result->allow_timing_details) {
result->server_timing =
PerformanceServerTiming::ParseServerTimingToMojo(info);
}
if (!result->server_timing.IsEmpty()) {
UseCounter::Count(&context_for_use_counter,
WebFeature::kPerformanceServerTiming);
}
return result;
}
void Performance::AddResourceTiming(
mojom::blink::ResourceTimingInfoPtr info,
const AtomicString& initiator_type,
mojo::PendingReceiver<mojom::blink::WorkerTimingContainer>
worker_timing_receiver,
ExecutionContext* context) {
auto* entry = MakeGarbageCollected<PerformanceResourceTiming>(
*info, time_origin_, initiator_type, std::move(worker_timing_receiver),
context);
NotifyObserversOfEntry(*entry);
// https://w3c.github.io/resource-timing/#dfn-add-a-performanceresourcetiming-entry
if (CanAddResourceTimingEntry() &&
!resource_timing_buffer_full_event_pending_) {
resource_timing_buffer_.push_back(entry);
return;
}
if (!resource_timing_buffer_full_event_pending_) {
resource_timing_buffer_full_event_pending_ = true;
resource_timing_buffer_full_timer_.StartOneShot(base::TimeDelta(),
FROM_HERE);
}
resource_timing_secondary_buffer_.push_back(entry);
}
// Called after loadEventEnd happens.
void Performance::NotifyNavigationTimingToObservers() {
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
if (navigation_timing_)
NotifyObserversOfEntry(*navigation_timing_);
}
bool Performance::IsElementTimingBufferFull() const {
return element_timing_buffer_.size() >= element_timing_buffer_max_size_;
}
bool Performance::IsEventTimingBufferFull() const {
return event_timing_buffer_.size() >= event_timing_buffer_max_size_;
}
void Performance::CopySecondaryBuffer() {
// https://w3c.github.io/resource-timing/#dfn-copy-secondary-buffer
while (!resource_timing_secondary_buffer_.empty() &&
CanAddResourceTimingEntry()) {
PerformanceEntry* entry = resource_timing_secondary_buffer_.front();
DCHECK(entry);
resource_timing_secondary_buffer_.pop_front();
resource_timing_buffer_.push_back(entry);
}
}
void Performance::FireResourceTimingBufferFull(TimerBase*) {
// https://w3c.github.io/resource-timing/#dfn-fire-a-buffer-full-event
while (!resource_timing_secondary_buffer_.empty()) {
int excess_entries_before = resource_timing_secondary_buffer_.size();
if (!CanAddResourceTimingEntry()) {
DispatchEvent(
*Event::Create(event_type_names::kResourcetimingbufferfull));
}
CopySecondaryBuffer();
int excess_entries_after = resource_timing_secondary_buffer_.size();
if (excess_entries_after >= excess_entries_before) {
resource_timing_secondary_buffer_.clear();
break;
}
}
resource_timing_buffer_full_event_pending_ = false;
}
void Performance::AddElementTimingBuffer(PerformanceElementTiming& entry) {
if (!IsElementTimingBufferFull()) {
element_timing_buffer_.push_back(&entry);
}
}
void Performance::AddEventTimingBuffer(PerformanceEventTiming& entry) {
DCHECK(RuntimeEnabledFeatures::EventTimingEnabled(GetExecutionContext()));
if (!IsEventTimingBufferFull()) {
event_timing_buffer_.push_back(&entry);
}
}
void Performance::AddLayoutShiftBuffer(LayoutShift& entry) {
probe::PerformanceEntryAdded(GetExecutionContext(), &entry);
if (layout_shift_buffer_.size() < kDefaultLayoutShiftBufferSize)
layout_shift_buffer_.push_back(&entry);
}
void Performance::AddLargestContentfulPaint(LargestContentfulPaint* entry) {
probe::PerformanceEntryAdded(GetExecutionContext(), entry);
if (largest_contentful_paint_buffer_.size() <
kDefaultLargestContenfulPaintSize) {
largest_contentful_paint_buffer_.push_back(entry);
}
}
void Performance::AddFirstPaintTiming(base::TimeTicks start_time) {
AddPaintTiming(PerformancePaintTiming::PaintType::kFirstPaint, start_time);
}
void Performance::AddFirstContentfulPaintTiming(base::TimeTicks start_time) {
AddPaintTiming(PerformancePaintTiming::PaintType::kFirstContentfulPaint,
start_time);
}
void Performance::AddPaintTiming(PerformancePaintTiming::PaintType type,
base::TimeTicks start_time) {
PerformanceEntry* entry = MakeGarbageCollected<PerformancePaintTiming>(
type, MonotonicTimeToDOMHighResTimeStamp(start_time));
// Always buffer First Paint & First Contentful Paint.
if (type == PerformancePaintTiming::PaintType::kFirstPaint)
first_paint_timing_ = entry;
else if (type == PerformancePaintTiming::PaintType::kFirstContentfulPaint)
first_contentful_paint_timing_ = entry;
NotifyObserversOfEntry(*entry);
}
bool Performance::CanAddResourceTimingEntry() {
// https://w3c.github.io/resource-timing/#dfn-can-add-resource-timing-entry
return resource_timing_buffer_.size() < resource_timing_buffer_size_limit_;
}
void Performance::AddLongTaskTiming(base::TimeTicks start_time,
base::TimeTicks end_time,
const AtomicString& name,
const AtomicString& container_type,
const AtomicString& container_src,
const AtomicString& container_id,
const AtomicString& container_name) {
double dom_high_res_start_time =
MonotonicTimeToDOMHighResTimeStamp(start_time);
auto* entry = MakeGarbageCollected<PerformanceLongTaskTiming>(
dom_high_res_start_time,
// Convert the delta between start and end times to an int to reduce the
// granularity of the duration to 1 ms.
static_cast<int>(MonotonicTimeToDOMHighResTimeStamp(end_time) -
dom_high_res_start_time),
name, container_type, container_src, container_id, container_name);
if (longtask_buffer_.size() < kDefaultLongTaskBufferSize) {
longtask_buffer_.push_back(entry);
} else {
UseCounter::Count(GetExecutionContext(), WebFeature::kLongTaskBufferFull);
}
NotifyObserversOfEntry(*entry);
}
UserTiming& Performance::GetUserTiming() {
if (!user_timing_)
user_timing_ = MakeGarbageCollected<UserTiming>(*this);
return *user_timing_;
}
PerformanceMark* Performance::mark(ScriptState* script_state,
const AtomicString& mark_name,
PerformanceMarkOptions* mark_options,
ExceptionState& exception_state) {
if (mark_options &&
(mark_options->hasStartTime() || mark_options->hasDetail())) {
UseCounter::Count(GetExecutionContext(), WebFeature::kUserTimingL3);
}
PerformanceMark* performance_mark = PerformanceMark::Create(
script_state, mark_name, mark_options, exception_state);
if (performance_mark) {
GetUserTiming().AddMarkToPerformanceTimeline(*performance_mark);
NotifyObserversOfEntry(*performance_mark);
}
return performance_mark;
}
void Performance::clearMarks(const AtomicString& mark_name) {
GetUserTiming().ClearMarks(mark_name);
}
PerformanceMeasure* Performance::measure(ScriptState* script_state,
const AtomicString& measure_name,
ExceptionState& exception_state) {
// When |startOrOptions| is not provided, it's assumed to be an empty
// dictionary.
return MeasureInternal(
script_state, measure_name,
StringOrPerformanceMeasureOptions::FromPerformanceMeasureOptions(
PerformanceMeasureOptions::Create()),
base::nullopt, exception_state);
}
PerformanceMeasure* Performance::measure(
ScriptState* script_state,
const AtomicString& measure_name,
const StringOrPerformanceMeasureOptions& start_or_options,
ExceptionState& exception_state) {
return MeasureInternal(script_state, measure_name, start_or_options,
base::nullopt, exception_state);
}
PerformanceMeasure* Performance::measure(
ScriptState* script_state,
const AtomicString& measure_name,
const StringOrPerformanceMeasureOptions& start_or_options,
const String& end,
ExceptionState& exception_state) {
return MeasureInternal(script_state, measure_name, start_or_options,
base::Optional<String>(end), exception_state);
}
// |MeasureInternal| exists to unify the arguments from different
// `performance.measure()` overloads into a consistent form, then delegate to
// |MeasureWithDetail|.
//
// |start_or_options| is either a String or a dictionary of options. When it's
// a String, it represents a starting performance mark. When it's a dictionary,
// the allowed fields are 'start', 'duration', 'end' and 'detail'. However,
// there are some combinations of fields and parameters which must raise
// errors. Specifically, the spec (https://https://w3c.github.io/user-timing/)
// requires errors to thrown in the following cases:
// - If |start_or_options| is a dictionary and 'end_mark' is passed.
// - If an options dictionary contains neither a 'start' nor an 'end' field.
// - If an options dictionary contains all of 'start', 'duration' and 'end'.
//
// |end_mark| will be base::nullopt unless the `performance.measure()` overload
// specified an end mark.
PerformanceMeasure* Performance::MeasureInternal(
ScriptState* script_state,
const AtomicString& measure_name,
const StringOrPerformanceMeasureOptions& start_or_options,
base::Optional<String> end_mark,
ExceptionState& exception_state) {
DCHECK(!start_or_options.IsNull());
// An empty option is treated with no difference as null, undefined.
if (start_or_options.IsPerformanceMeasureOptions() &&
!IsMeasureOptionsEmpty(
*start_or_options.GetAsPerformanceMeasureOptions())) {
UseCounter::Count(GetExecutionContext(), WebFeature::kUserTimingL3);
// measure("name", { start, end }, *)
if (end_mark) {
exception_state.ThrowTypeError(
"If a non-empty PerformanceMeasureOptions object was passed, "
"|end_mark| must not be passed.");
return nullptr;
}
const PerformanceMeasureOptions* options =
start_or_options.GetAsPerformanceMeasureOptions();
if (!options->hasStart() && !options->hasEnd()) {
exception_state.ThrowTypeError(
"If a non-empty PerformanceMeasureOptions object was passed, at "
"least one of its 'start' or 'end' properties must be present.");
return nullptr;
}
if (options->hasStart() && options->hasDuration() && options->hasEnd()) {
exception_state.ThrowTypeError(
"If a non-empty PerformanceMeasureOptions object was passed, it "
"must not have all of its 'start', 'duration', and 'end' "
"properties defined");
return nullptr;
}
base::Optional<StringOrDouble> start;
if (options->hasStart()) {
start = options->start();
}
base::Optional<double> duration;
if (options->hasDuration()) {
duration = options->duration();
}
base::Optional<StringOrDouble> end;
if (options->hasEnd()) {
end = options->end();
}
return MeasureWithDetail(
script_state, measure_name, start, duration, end,
options->hasDetail() ? options->detail() : ScriptValue(),
exception_state);
}
// measure("name", "mark1", *)
base::Optional<StringOrDouble> start;
if (start_or_options.IsString()) {
start = StringOrDouble::FromString(start_or_options.GetAsString());
}
// We let |end_mark| behave the same whether it's empty, undefined or null
// in JS, as long as |end_mark| is null in C++.
base::Optional<StringOrDouble> end;
if (end_mark) {
end = StringOrDouble::FromString(*end_mark);
}
return MeasureWithDetail(script_state, measure_name, start,
/* duration = */ base::nullopt, end,
ScriptValue::CreateNull(script_state->GetIsolate()),
exception_state);
}
PerformanceMeasure* Performance::MeasureWithDetail(
ScriptState* script_state,
const AtomicString& measure_name,
const base::Optional<StringOrDouble>& start,
const base::Optional<double>& duration,
const base::Optional<StringOrDouble>& end,
const ScriptValue& detail,
ExceptionState& exception_state) {
PerformanceMeasure* performance_measure =
GetUserTiming().Measure(script_state, measure_name, start, duration, end,
detail, exception_state);
if (performance_measure)
NotifyObserversOfEntry(*performance_measure);
return performance_measure;
}
void Performance::clearMeasures(const AtomicString& measure_name) {
GetUserTiming().ClearMeasures(measure_name);
}
ScriptPromise Performance::profile(ScriptState* script_state,
const ProfilerInitOptions* options,
ExceptionState& exception_state) {
auto* execution_context = ExecutionContext::From(script_state);
DCHECK(execution_context);
DCHECK(
RuntimeEnabledFeatures::ExperimentalJSProfilerEnabled(execution_context));
if (!GetExecutionContext()->IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kJSProfiling,
ReportOptions::kReportOnFailure)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotAllowedError,
"JS profiling is disabled by Document Policy.");
return ScriptPromise();
}
// Bypass COOP/COEP checks when the |--disable-web-security| flag is present.
bool web_security_enabled = true;
if (LocalDOMWindow* window = LocalDOMWindow::From(script_state)) {
if (LocalFrame* local_frame = window->GetFrame()) {
web_security_enabled =
local_frame->GetSettings()->GetWebSecurityEnabled();
}
}
if (web_security_enabled &&
!execution_context->CrossOriginIsolatedCapability()) {
exception_state.ThrowSecurityError(
"performance.profile() requires COOP+COEP (web.dev/coop-coep)");
return ScriptPromise();
}
auto* profiler_group = ProfilerGroup::From(script_state->GetIsolate());
DCHECK(profiler_group);
auto* profiler = profiler_group->CreateProfiler(
script_state, *options, time_origin_, exception_state);
if (exception_state.HadException())
return ScriptPromise();
return ScriptPromise::Cast(script_state, ToV8(profiler, script_state));
}
void Performance::RegisterPerformanceObserver(PerformanceObserver& observer) {
observer_filter_options_ |= observer.FilterOptions();
observers_.insert(&observer);
}
void Performance::UnregisterPerformanceObserver(
PerformanceObserver& old_observer) {
observers_.erase(&old_observer);
UpdatePerformanceObserverFilterOptions();
}
void Performance::UpdatePerformanceObserverFilterOptions() {
observer_filter_options_ = PerformanceEntry::kInvalid;
for (const auto& observer : observers_) {
observer_filter_options_ |= observer->FilterOptions();
}
}
void Performance::NotifyObserversOfEntry(PerformanceEntry& entry) const {
DCHECK(entry.EntryTypeEnum() != PerformanceEntry::kEvent ||
RuntimeEnabledFeatures::EventTimingEnabled(GetExecutionContext()));
bool observer_found = false;
for (auto& observer : observers_) {
if (observer->FilterOptions() & entry.EntryTypeEnum() &&
observer->CanObserve(entry)) {
observer->EnqueuePerformanceEntry(entry);
observer_found = true;
}
}
if (observer_found && entry.EntryTypeEnum() == PerformanceEntry::kPaint)
UseCounter::Count(GetExecutionContext(), WebFeature::kPaintTimingObserved);
}
bool Performance::HasObserverFor(
PerformanceEntry::EntryType filter_type) const {
return observer_filter_options_ & filter_type;
}
void Performance::ActivateObserver(PerformanceObserver& observer) {
if (active_observers_.IsEmpty())
deliver_observations_timer_.StartOneShot(base::TimeDelta(), FROM_HERE);
if (suspended_observers_.Contains(&observer))
suspended_observers_.erase(&observer);
active_observers_.insert(&observer);
}
void Performance::SuspendObserver(PerformanceObserver& observer) {
DCHECK(!suspended_observers_.Contains(&observer));
if (!active_observers_.Contains(&observer))
return;
active_observers_.erase(&observer);
suspended_observers_.insert(&observer);
}
void Performance::DeliverObservationsTimerFired(TimerBase*) {
decltype(active_observers_) observers;
active_observers_.Swap(observers);
for (const auto& observer : observers)
observer->Deliver();
}
// static
double Performance::ClampTimeResolution(double time_seconds) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(TimeClamper, clamper, ());
return clamper.ClampTimeResolution(time_seconds);
}
// static
DOMHighResTimeStamp Performance::MonotonicTimeToDOMHighResTimeStamp(
base::TimeTicks time_origin,
base::TimeTicks monotonic_time,
bool allow_negative_value) {
// Avoid exposing raw platform timestamps.
if (monotonic_time.is_null() || time_origin.is_null())
return 0.0;
double clamped_time_in_seconds =
ClampTimeResolution(monotonic_time.since_origin().InSecondsF()) -
ClampTimeResolution(time_origin.since_origin().InSecondsF());
if (clamped_time_in_seconds < 0 && !allow_negative_value)
return 0.0;
return ConvertSecondsToDOMHighResTimeStamp(clamped_time_in_seconds);
}
// static
base::TimeDelta Performance::MonotonicTimeToTimeDelta(
base::TimeTicks time_origin,
base::TimeTicks monotonic_time,
bool allow_negative_value) {
return base::TimeDelta::FromMillisecondsD(MonotonicTimeToDOMHighResTimeStamp(
time_origin, monotonic_time, allow_negative_value));
}
DOMHighResTimeStamp Performance::MonotonicTimeToDOMHighResTimeStamp(
base::TimeTicks monotonic_time) const {
return MonotonicTimeToDOMHighResTimeStamp(time_origin_, monotonic_time,
false /* allow_negative_value */);
}
base::TimeDelta Performance::MonotonicTimeToTimeDelta(
base::TimeTicks monotonic_time) const {
return MonotonicTimeToTimeDelta(time_origin_, monotonic_time,
false /* allow_negative_value */);
}
DOMHighResTimeStamp Performance::now() const {
return MonotonicTimeToDOMHighResTimeStamp(tick_clock_->NowTicks());
}
// static
bool Performance::CanExposeNode(Node* node) {
if (!node || !node->isConnected() || node->IsInShadowTree())
return false;
// Do not expose |node| when the document is not 'fully active'.
const Document& document = node->GetDocument();
if (!document.IsActive() || !document.GetFrame())
return false;
return true;
}
ScriptValue Performance::toJSONForBinding(ScriptState* script_state) const {
V8ObjectBuilder result(script_state);
BuildJSONValue(result);
return result.GetScriptValue();
}
void Performance::BuildJSONValue(V8ObjectBuilder& builder) const {
builder.AddNumber("timeOrigin", timeOrigin());
// |memory| is not part of the spec, omitted.
}
void Performance::Trace(Visitor* visitor) const {
visitor->Trace(resource_timing_buffer_);
visitor->Trace(resource_timing_secondary_buffer_);
visitor->Trace(element_timing_buffer_);
visitor->Trace(event_timing_buffer_);
visitor->Trace(layout_shift_buffer_);
visitor->Trace(largest_contentful_paint_buffer_);
visitor->Trace(longtask_buffer_);
visitor->Trace(visibility_state_buffer_);
visitor->Trace(navigation_timing_);
visitor->Trace(user_timing_);
visitor->Trace(first_paint_timing_);
visitor->Trace(first_contentful_paint_timing_);
visitor->Trace(first_input_timing_);
visitor->Trace(observers_);
visitor->Trace(active_observers_);
visitor->Trace(suspended_observers_);
visitor->Trace(deliver_observations_timer_);
visitor->Trace(resource_timing_buffer_full_timer_);
EventTargetWithInlineData::Trace(visitor);
}
void Performance::SetClocksForTesting(const base::Clock* clock,
const base::TickClock* tick_clock) {
tick_clock_ = tick_clock;
// Recompute |unix_at_zero_monotonic_|.
unix_at_zero_monotonic_ = ConvertSecondsToDOMHighResTimeStamp(
clock->Now().ToDoubleT() -
tick_clock_->NowTicks().since_origin().InSecondsF());
}
void Performance::ResetTimeOriginForTesting(base::TimeTicks time_origin) {
time_origin_ = time_origin;
}
} // namespace blink