diff --git a/third_party/blink/renderer/bindings/bindings.gni b/third_party/blink/renderer/bindings/bindings.gni index a6fe05244bcf3..7bc66f82451b3 100644 --- a/third_party/blink/renderer/bindings/bindings.gni +++ b/third_party/blink/renderer/bindings/bindings.gni @@ -97,6 +97,7 @@ blink_core_sources_bindings = "core/v8/script_promise.cc", "core/v8/script_promise.h", "core/v8/script_promise_property.h", + "core/v8/script_promise_result_tracker.h", "core/v8/script_promise_resolver.cc", "core/v8/script_promise_resolver.h", "core/v8/script_regexp.cc", @@ -221,6 +222,7 @@ bindings_unittest_files = get_path_info( "core/v8/referrer_script_info_test.cc", "core/v8/script_promise_property_test.cc", "core/v8/script_promise_resolver_test.cc", + "core/v8/script_promise_result_tracker_test.cc", "core/v8/script_promise_test.cc", "core/v8/script_streamer_test.cc", "core/v8/script_wrappable_v8_gc_integration_test.cc", diff --git a/third_party/blink/renderer/bindings/core/v8/script_promise_result_tracker.h b/third_party/blink/renderer/bindings/core/v8/script_promise_result_tracker.h new file mode 100644 index 0000000000000..e2d13a8041cb2 --- /dev/null +++ b/third_party/blink/renderer/bindings/core/v8/script_promise_result_tracker.h @@ -0,0 +1,115 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_BINDINGS_CORE_V8_SCRIPT_PROMISE_RESULT_TRACKER_H_ +#define THIRD_PARTY_BLINK_RENDERER_BINDINGS_CORE_V8_SCRIPT_PROMISE_RESULT_TRACKER_H_ + +#include "base/metrics/histogram_functions.h" +#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" + +namespace blink { + +// ScriptPromiseResultTracker is a wrapper around ScriptPromiseResolver which +// simplifies recording UMA metric and latency for APIs. + +// Callers should ensure that the ResultEnumType has kOk and kTimedOut as +// values. +template +class CORE_EXPORT ScriptPromiseResultTracker + : public GarbageCollected> { + public: + // If the targeted histograms are "WebRTC.EnumerateDevices.Result" and + // "WebRTC.EnumerateDevices.Latency", the input to |metric_name_prefix| should + // be "WebRTC.EnumerateDevices". + // + // |timeout_interval| is the timeout limit after which a + // ResultEnumType::kTimedOut response is recorded in the Result histogram. + // + // This creates/accesses the Latency histogram which has |n_buckets_| buckets + // and the range of the buckets are from (min_latency_bucket, + // max_latency_bucket). + ScriptPromiseResultTracker( + ScriptState* script_state, + std::string metric_name_prefix, + base::TimeDelta timeout_interval, + base::TimeDelta min_latency_bucket = base::Milliseconds(1), + base::TimeDelta max_latency_bucket = base::Seconds(10), + size_t n_buckets = 50) + : metric_name_prefix_(std::move(metric_name_prefix)), + start_time_(base::TimeTicks::Now()), + timeout_interval_(timeout_interval), + min_latency_bucket_(min_latency_bucket), + max_latency_bucket_(max_latency_bucket), + n_buckets_(n_buckets) { + CHECK(!metric_name_prefix_.empty()); + resolver_ = MakeGarbageCollected(script_state); + if (timeout_interval.is_positive()) { + ExecutionContext::From(script_state) + ->GetTaskRunner(TaskType::kInternalDefault) + ->PostDelayedTask( + FROM_HERE, + WTF::BindOnce(&ScriptPromiseResultTracker::RecordResult, + WrapPersistent(this), ResultEnumType::kTimedOut), + timeout_interval); + } + } + ScriptPromiseResultTracker(const ScriptPromiseResultTracker&) = delete; + ScriptPromiseResultTracker& operator=(const ScriptPromiseResultTracker&) = + delete; + ~ScriptPromiseResultTracker() = default; + + template + void Resolve(T value, ResultEnumType result = ResultEnumType::kOk) { + RecordResult(result); + RecordLatency(); + resolver_->Resolve(value); + } + + template + void Reject(T value, ResultEnumType result) { + RecordResult(result); + RecordLatency(); + resolver_->Reject(value); + } + + void RecordResult(ResultEnumType result) { + if (is_result_recorded_) + return; + + is_result_recorded_ = true; + base::UmaHistogramEnumeration(metric_name_prefix_ + ".Result", result); + } + + void RecordLatency() { + if (is_latency_recorded_) + return; + + is_latency_recorded_ = true; + const base::TimeDelta elapsed = base::TimeTicks::Now() - start_time_; + base::UmaHistogramCustomTimes(metric_name_prefix_ + ".Latency", elapsed, + min_latency_bucket_, max_latency_bucket_, + n_buckets_); + } + + ScriptState* GetScriptState() const { return resolver_->GetScriptState(); } + + ScriptPromise Promise() { return resolver_->Promise(); } + + void Trace(Visitor* visitor) const { visitor->Trace(resolver_); } + + private: + Member resolver_; + std::string metric_name_prefix_; + base::TimeTicks start_time_; + base::TimeDelta timeout_interval_; + base::TimeDelta min_latency_bucket_; + base::TimeDelta max_latency_bucket_; + size_t n_buckets_; + bool is_latency_recorded_ = false; + bool is_result_recorded_ = false; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_BINDINGS_CORE_V8_SCRIPT_PROMISE_RESULT_TRACKER_H_ diff --git a/third_party/blink/renderer/bindings/core/v8/script_promise_result_tracker_test.cc b/third_party/blink/renderer/bindings/core/v8/script_promise_result_tracker_test.cc new file mode 100644 index 0000000000000..d26d2d794ef48 --- /dev/null +++ b/third_party/blink/renderer/bindings/core/v8/script_promise_result_tracker_test.cc @@ -0,0 +1,181 @@ +// Copyright 2022 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/bindings/core/v8/script_promise_result_tracker.h" + +#include "base/test/metrics/histogram_tester.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/bindings/core/v8/script_function.h" +#include "third_party/blink/renderer/bindings/core/v8/script_value.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/testing/dummy_page_holder.h" +#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" +#include "v8/include/v8.h" + +namespace blink { + +class TestHelperFunction : public ScriptFunction::Callable { + public: + explicit TestHelperFunction(String* value) : value_(value) {} + + ScriptValue Call(ScriptState* script_state, ScriptValue value) override { + DCHECK(!value.IsEmpty()); + *value_ = ToCoreString( + value.V8Value()->ToString(script_state->GetContext()).ToLocalChecked()); + return value; + } + + private: + String* value_; +}; + +enum class TestEnum { + kOk = 0, + kFailedWithReason = 1, + kTimedOut = 2, + kMaxValue = kTimedOut +}; + +class ScriptPromiseResultTrackerTest : public testing::Test { + public: + ScriptPromiseResultTrackerTest() + : metric_name_prefix_("Histogram.TestEnum"), + page_holder_(std::make_unique()) {} + + ~ScriptPromiseResultTrackerTest() override { PerformMicrotaskCheckpoint(); } + + ScriptState* GetScriptState() const { + return ToScriptStateForMainWorld(&page_holder_->GetFrame()); + } + + void PerformMicrotaskCheckpoint() { + ScriptState::Scope scope(GetScriptState()); + GetScriptState()->GetContext()->GetMicrotaskQueue()->PerformCheckpoint( + GetScriptState()->GetIsolate()); + } + + ScriptPromiseResultTracker* CreateResultTracker( + String& on_fulfilled, + String& on_rejected, + base::TimeDelta timeout_delay = base::Minutes(1)) { + ScriptState::Scope scope(GetScriptState()); + auto* result_tracker = + MakeGarbageCollected>( + GetScriptState(), metric_name_prefix_, timeout_delay); + + ScriptPromise promise = result_tracker->Promise(); + promise.Then(MakeGarbageCollected( + GetScriptState(), + MakeGarbageCollected(&on_fulfilled)), + MakeGarbageCollected( + GetScriptState(), + MakeGarbageCollected(&on_rejected))); + + PerformMicrotaskCheckpoint(); + + CheckResultHistogram(/*expected_count=*/0); + CheckLatencyHistogram(/*expected_count=*/0); + return result_tracker; + } + + void CheckResultHistogram(int expected_count) { + histogram_tester_.ExpectTotalCount(metric_name_prefix_ + ".Result", + expected_count); + } + + void CheckLatencyHistogram(int expected_count) { + histogram_tester_.ExpectTotalCount(metric_name_prefix_ + ".Latency", + expected_count); + } + + protected: + base::HistogramTester histogram_tester_; + std::string metric_name_prefix_; + std::unique_ptr page_holder_; +}; + +TEST_F(ScriptPromiseResultTrackerTest, resolve) { + String on_fulfilled, on_rejected; + auto* result_tracker = CreateResultTracker(on_fulfilled, on_rejected); + result_tracker->Resolve(/*value=*/"hello", /*result=*/TestEnum::kOk); + PerformMicrotaskCheckpoint(); + + EXPECT_EQ("hello", on_fulfilled); + EXPECT_EQ(String(), on_rejected); + CheckResultHistogram(/*expected_count=*/1); + CheckLatencyHistogram(/*expected_count=*/1); +} + +TEST_F(ScriptPromiseResultTrackerTest, reject) { + String on_fulfilled, on_rejected; + auto* result_tracker = CreateResultTracker(on_fulfilled, on_rejected); + result_tracker->Reject(/*value=*/"hello", + /*result=*/TestEnum::kFailedWithReason); + PerformMicrotaskCheckpoint(); + + EXPECT_EQ(String(), on_fulfilled); + EXPECT_EQ("hello", on_rejected); + CheckResultHistogram(/*expected_count=*/1); + CheckLatencyHistogram(/*expected_count=*/1); +} + +TEST_F(ScriptPromiseResultTrackerTest, resolve_reject_again) { + String on_fulfilled, on_rejected; + auto* result_tracker = CreateResultTracker(on_fulfilled, on_rejected); + result_tracker->Reject(/*value=*/"hello", + /*result=*/TestEnum::kFailedWithReason); + PerformMicrotaskCheckpoint(); + + EXPECT_EQ(String(), on_fulfilled); + EXPECT_EQ("hello", on_rejected); + CheckResultHistogram(/*expected_count=*/1); + CheckLatencyHistogram(/*expected_count=*/1); + + // Resolve/Reject on already resolved/rejected promise doesn't log new values + // in the histogram. + result_tracker->Resolve(/*value=*/"bye", /*result=*/TestEnum::kOk); + result_tracker->Reject(/*value=*/"bye", + /*result=*/TestEnum::kFailedWithReason); + PerformMicrotaskCheckpoint(); + + EXPECT_EQ(String(), on_fulfilled); + EXPECT_EQ("hello", on_rejected); + CheckResultHistogram(/*expected_count=*/1); + CheckLatencyHistogram(/*expected_count=*/1); +} + +TEST_F(ScriptPromiseResultTrackerTest, timeout) { + String on_fulfilled, on_rejected; + base::TimeDelta timeout_delay = base::Milliseconds(200); + auto* result_tracker = + CreateResultTracker(on_fulfilled, on_rejected, timeout_delay); + + // Run the tasks scheduled to run within the delay specified. + test::RunDelayedTasks(timeout_delay); + PerformMicrotaskCheckpoint(); + + // kTimedOut is logged in the Result histogram but nothing is logged in the + // latency histogram as the promise was never rejected or resolved. + CheckResultHistogram(/*expected_count=*/1); + CheckLatencyHistogram(/*expected_count=*/0); + + // Though the timeout has passed, the promise is not yet rejected or resolved. + EXPECT_EQ(String(), on_fulfilled); + EXPECT_EQ(String(), on_rejected); + + result_tracker->Reject(/*value=*/"hello", + /*result=*/TestEnum::kFailedWithReason); + PerformMicrotaskCheckpoint(); + + EXPECT_EQ("hello", on_rejected); + EXPECT_EQ(String(), on_fulfilled); + + // Rejected result is not logged again as it was rejected after the timeout + // had passed. It is still logged in the latency though. + CheckResultHistogram(/*expected_count=*/1); + CheckLatencyHistogram(/*expected_count=*/1); +} + +} // namespace blink