blob: 21ed9f3f44ae7728165b034388ab9731d7559fb9 [file] [log] [blame]
// 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/compute_pressure/pressure_observer.h"
#include <algorithm>
#include "base/task/sequenced_task_runner.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h"
#include "third_party/blink/public/common/features.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/modules/v8/v8_pressure_observer_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_pressure_record.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_pressure_source.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
#include "third_party/blink/renderer/modules/compute_pressure/pressure_observer_manager.h"
#include "third_party/blink/renderer/modules/compute_pressure/pressure_record.h"
#include "third_party/blink/renderer/modules/compute_pressure/pressure_source_index.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
namespace blink {
namespace {
constexpr char kFeaturePolicyBlocked[] =
"Access to the feature \"compute pressure\" is disallowed by permissions "
"policy.";
} // namespace
PressureObserver::PressureObserver(V8PressureUpdateCallback* observer_callback)
: observer_callback_(observer_callback) {}
PressureObserver::~PressureObserver() = default;
// static
PressureObserver* PressureObserver::Create(V8PressureUpdateCallback* callback) {
return MakeGarbageCollected<PressureObserver>(callback);
}
// static
Vector<V8PressureSource> PressureObserver::knownSources() {
return Vector<V8PressureSource>(
{V8PressureSource(V8PressureSource::Enum::kCpu)});
}
ScriptPromise<IDLUndefined> PressureObserver::observe(
ScriptState* script_state,
V8PressureSource source,
PressureObserverOptions* options,
ExceptionState& exception_state) {
ExecutionContext* execution_context = ExecutionContext::From(script_state);
if (execution_context->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return EmptyPromise();
}
// Checks whether the document is allowed by Permissions Policy to call
// Compute Pressure API.
if (!execution_context->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kComputePressure,
ReportOptions::kReportOnFailure)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotAllowedError,
kFeaturePolicyBlocked);
return EmptyPromise();
}
sample_interval_ = options->sampleInterval();
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
script_state, exception_state.GetContext());
pending_resolvers_[ToSourceIndex(source.AsEnum())].insert(resolver);
if (!manager_) {
manager_ = PressureObserverManager::From(execution_context);
}
manager_->AddObserver(source.AsEnum(), this);
return resolver->Promise();
}
void PressureObserver::unobserve(V8PressureSource source) {
// Wrong order of calls.
if (!manager_) {
return;
}
const auto source_index = ToSourceIndex(source.AsEnum());
// https://w3c.github.io/compute-pressure/#the-unobserve-method
manager_->RemoveObserver(source.AsEnum(), this);
last_record_map_[source_index].Clear();
after_penalty_records_[source_index].Clear();
pending_delayed_report_to_callback_[source_index].Cancel();
// Reject all pending promises for `source`.
RejectPendingResolvers(source.AsEnum(), DOMExceptionCode::kAbortError,
"Called unobserve method.");
auto removed = std::ranges::remove(records_, source, &PressureRecord::source);
records_.erase(removed.begin(), removed.end());
}
void PressureObserver::disconnect() {
// Wrong order of calls.
if (!manager_) {
return;
}
// https://w3c.github.io/compute-pressure/#the-disconnect-method
manager_->RemoveObserverFromAllSources(this);
for (auto& last_record : last_record_map_) {
last_record.Clear();
}
for (auto& after_penalty_record : after_penalty_records_) {
after_penalty_record.Clear();
}
for (auto& pending_callback : pending_delayed_report_to_callback_) {
pending_callback.Cancel();
}
// Reject all pending promises.
for (const auto& source : knownSources()) {
RejectPendingResolvers(source.AsEnum(), DOMExceptionCode::kAbortError,
"Called disconnect method.");
}
records_.clear();
}
void PressureObserver::Trace(blink::Visitor* visitor) const {
visitor->Trace(manager_);
visitor->Trace(observer_callback_);
for (const auto& after_penalty_record : after_penalty_records_) {
visitor->Trace(after_penalty_record);
}
for (const auto& last_record : last_record_map_) {
visitor->Trace(last_record);
}
for (const auto& pending_resolver_set : pending_resolvers_) {
visitor->Trace(pending_resolver_set);
}
visitor->Trace(records_);
ScriptWrappable::Trace(visitor);
}
void PressureObserver::OnUpdate(ExecutionContext* execution_context,
V8PressureSource::Enum source,
V8PressureState::Enum state,
double own_contribution_estimate,
DOMHighResTimeStamp timestamp) {
if (!PassesRateTest(source, timestamp)) {
return;
}
if (!ShouldDispatch(source, state, own_contribution_estimate)) {
return;
}
auto* record = MakeGarbageCollected<PressureRecord>(
source, state, own_contribution_estimate, timestamp);
if (base::FeatureList::IsEnabled(
features::kComputePressureRateObfuscationMitigation)) {
const auto source_index = ToSourceIndex(source);
// Steps 4.5.1 and 4.5.2
// https://w3c.github.io/compute-pressure/#dfn-data-delivery
if (pending_delayed_report_to_callback_[source_index].IsActive()) {
after_penalty_records_[source_index] = record;
return;
}
change_rate_monitor_.ResetIfNeeded();
change_rate_monitor_.IncreaseChangeCount(source);
if (!PassesRateObfuscation(source)) {
// Steps 4.6.1 and 4.6.2
// https://w3c.github.io/compute-pressure/#dfn-data-delivery
after_penalty_records_[source_index] = record;
pending_delayed_report_to_callback_[source_index] =
PostDelayedCancellableTask(
*execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI),
FROM_HERE,
BindOnce(&PressureObserver::QueueAfterPenaltyRecord,
WrapWeakPersistent(this),
WrapWeakPersistent(execution_context), source),
change_rate_monitor_.penalty_duration());
change_rate_monitor_.ResetChangeCount(source);
return;
}
}
QueuePressureRecord(execution_context, source, record);
}
// Steps 4.6.3.1.1-3 of
// https://w3c.github.io/compute-pressure/#dfn-data-delivery
void PressureObserver::QueueAfterPenaltyRecord(
ExecutionContext* execution_context,
V8PressureSource::Enum source) {
const auto source_index = ToSourceIndex(source);
CHECK(after_penalty_records_[source_index]);
auto& record = after_penalty_records_[source_index];
QueuePressureRecord(execution_context, source, record);
}
// https://w3c.github.io/compute-pressure/#queue-a-pressurerecord
void PressureObserver::QueuePressureRecord(ExecutionContext* execution_context,
V8PressureSource::Enum source,
PressureRecord* record) {
// This should happen infrequently since `records_` is supposed
// to be emptied at every callback invoking or takeRecords().
if (records_.size() >= kMaxQueuedRecords) {
records_.erase(records_.begin());
}
records_.push_back(record);
CHECK_LE(records_.size(), kMaxQueuedRecords);
last_record_map_[ToSourceIndex(source)] = record;
if (pending_report_to_callback_.IsActive()) {
return;
}
pending_report_to_callback_ = PostCancellableTask(
*execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI), FROM_HERE,
BindOnce(&PressureObserver::ReportToCallback, WrapWeakPersistent(this),
WrapWeakPersistent(execution_context)));
}
void PressureObserver::OnBindingSucceeded(V8PressureSource::Enum source) {
ResolvePendingResolvers(source);
}
void PressureObserver::OnBindingFailed(V8PressureSource::Enum source,
DOMExceptionCode exception_code) {
RejectPendingResolvers(source, exception_code,
"Not available on this platform.");
}
void PressureObserver::OnConnectionError() {
for (const auto& source : knownSources()) {
RejectPendingResolvers(source.AsEnum(),
DOMExceptionCode::kNotSupportedError,
"Connection error.");
}
}
void PressureObserver::ReportToCallback(ExecutionContext* execution_context) {
CHECK(observer_callback_);
if (!execution_context || execution_context->IsContextDestroyed()) {
return;
}
// Cleared by takeRecords, for example.
if (records_.empty()) {
return;
}
HeapVector<Member<PressureRecord>, kMaxQueuedRecords> records;
records_.swap(records);
observer_callback_->InvokeAndReportException(this, records, this);
}
HeapVector<Member<PressureRecord>> PressureObserver::takeRecords() {
// This method clears records_.
HeapVector<Member<PressureRecord>, kMaxQueuedRecords> records;
records.swap(records_);
return records;
}
// https://w3c.github.io/compute-pressure/#dfn-passes-rate-test
bool PressureObserver::PassesRateTest(
V8PressureSource::Enum source,
const DOMHighResTimeStamp& timestamp) const {
const auto& last_record = last_record_map_[ToSourceIndex(source)];
if (!last_record) {
return true;
}
const double time_delta_milliseconds = timestamp - last_record->time();
return time_delta_milliseconds >= static_cast<double>(sample_interval_);
}
// https://w3c.github.io/compute-pressure/#dfn-should-dispach
bool PressureObserver::ShouldDispatch(V8PressureSource::Enum source,
V8PressureState::Enum state,
double own_contribution_estimate) const {
const auto& last_record = last_record_map_[ToSourceIndex(source)];
if (sample_interval_ != 0) {
return true;
}
if (!last_record) {
return true;
}
// conversion to std::optional for the comparison against last_record.
std::optional<double> maybe_estimate = own_contribution_estimate;
if (maybe_estimate < 0.0 || maybe_estimate > 1.0) {
maybe_estimate = std::nullopt;
}
return last_record->state() != state ||
last_record->ownContributionEstimate() != maybe_estimate;
}
// This function only checks the status of the rate obfuscation test.
// Incrementing of change count should happen before this call as described in
// https://w3c.github.io/compute-pressure/#dfn-passes-rate-obfuscation-test
bool PressureObserver::PassesRateObfuscation(
V8PressureSource::Enum source) const {
return !change_rate_monitor_.ChangeCountExceedsLimit(source);
}
void PressureObserver::ResolvePendingResolvers(V8PressureSource::Enum source) {
const auto source_index = ToSourceIndex(source);
for (const auto& resolver : pending_resolvers_[source_index]) {
resolver->Resolve();
}
pending_resolvers_[source_index].clear();
}
void PressureObserver::RejectPendingResolvers(V8PressureSource::Enum source,
DOMExceptionCode exception_code,
const String& message) {
const auto source_index = ToSourceIndex(source);
for (const auto& resolver : pending_resolvers_[source_index]) {
resolver->RejectWithDOMException(exception_code, message);
}
pending_resolvers_[source_index].clear();
}
} // namespace blink