Skip to content

Commit

Permalink
Define class ScriptPromiseResultTracker for metric tracking
Browse files Browse the repository at this point in the history
As part of the effort to improve metric and latency tracking of Chrome
RTC APIs, we have introduced the class ScriptPromiseResultTracker which wraps around ScriptPromiseResolver.

In the follow-up CLs, we would replace the usage of ScriptPromiseResolver with ScriptPromiseResultTracker for our APIs of interest.

Bug: 1373398
Change-Id: Ifa91ff641da1c703ec0f701a743c867af0203591
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4031635
Reviewed-by: Michael Lippautz <mlippautz@chromium.org>
Commit-Queue: Palak Agarwal <agpalak@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1075160}
  • Loading branch information
palak8669 authored and Chromium LUCI CQ committed Nov 23, 2022
1 parent 801a929 commit 272c037
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 0 deletions.
2 changes: 2 additions & 0 deletions third_party/blink/renderer/bindings/bindings.gni
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
@@ -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 <typename ResultEnumType>
class CORE_EXPORT ScriptPromiseResultTracker
: public GarbageCollected<ScriptPromiseResultTracker<ResultEnumType>> {
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<ScriptPromiseResolver>(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 <typename T>
void Resolve(T value, ResultEnumType result = ResultEnumType::kOk) {
RecordResult(result);
RecordLatency();
resolver_->Resolve(value);
}

template <typename T>
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<ScriptPromiseResolver> 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_
@@ -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<DummyPageHolder>()) {}

~ScriptPromiseResultTrackerTest() override { PerformMicrotaskCheckpoint(); }

ScriptState* GetScriptState() const {
return ToScriptStateForMainWorld(&page_holder_->GetFrame());
}

void PerformMicrotaskCheckpoint() {
ScriptState::Scope scope(GetScriptState());
GetScriptState()->GetContext()->GetMicrotaskQueue()->PerformCheckpoint(
GetScriptState()->GetIsolate());
}

ScriptPromiseResultTracker<TestEnum>* CreateResultTracker(
String& on_fulfilled,
String& on_rejected,
base::TimeDelta timeout_delay = base::Minutes(1)) {
ScriptState::Scope scope(GetScriptState());
auto* result_tracker =
MakeGarbageCollected<ScriptPromiseResultTracker<TestEnum>>(
GetScriptState(), metric_name_prefix_, timeout_delay);

ScriptPromise promise = result_tracker->Promise();
promise.Then(MakeGarbageCollected<ScriptFunction>(
GetScriptState(),
MakeGarbageCollected<TestHelperFunction>(&on_fulfilled)),
MakeGarbageCollected<ScriptFunction>(
GetScriptState(),
MakeGarbageCollected<TestHelperFunction>(&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<DummyPageHolder> 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

0 comments on commit 272c037

Please sign in to comment.