blob: b4a268c75a12a403c9554fd43ac2ca63b4413b35 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/events/event_ack_data.h"
#include <string>
#include <utility>
#include "base/containers/map_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/uuid.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_external_request_result.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_event_histogram_value.h"
namespace extensions {
EventAckData::EventInfo::EventInfo(
const base::Uuid& request_uuid,
int render_process_id,
int64_t version_id,
int worker_thread_id,
bool start_ok,
content::ServiceWorkerExternalRequestResult external_request_result,
base::TimeTicks dispatch_start_time,
EventDispatchSource dispatch_source,
bool lazy_background_active_on_dispatch,
const events::HistogramValue histogram_value)
: request_uuid(request_uuid),
render_process_id(render_process_id),
version_id(version_id),
worker_thread_id(worker_thread_id),
start_ok(start_ok),
external_request_result(external_request_result),
dispatch_start_time(dispatch_start_time),
dispatch_source(dispatch_source),
lazy_background_active_on_dispatch(lazy_background_active_on_dispatch),
histogram_value(histogram_value) {}
EventAckData::EventInfo::EventInfo(EventInfo&& other) = default;
EventAckData::EventAckData() = default;
EventAckData::~EventAckData() = default;
void EventAckData::IncrementInflightEvent(
content::ServiceWorkerContext* context,
int render_process_id,
int64_t version_id,
int worker_thread_id,
int event_id,
base::TimeTicks dispatch_start_time,
EventDispatchSource dispatch_source,
bool lazy_background_active_on_dispatch,
events::HistogramValue histogram_value) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::Uuid request_uuid = base::Uuid::GenerateRandomV4();
bool start_ok = true;
content::ServiceWorkerExternalRequestResult external_request_result =
context->StartingExternalRequest(
version_id,
content::ServiceWorkerExternalRequestTimeoutType::kDefault,
request_uuid);
base::UmaHistogramEnumeration(
"Extensions.ServiceWorkerBackground.StartingExternalRequest_Result",
external_request_result);
if (external_request_result !=
content::ServiceWorkerExternalRequestResult::kOk) {
LOG(ERROR) << "StartExternalRequest failed: "
<< static_cast<int>(external_request_result);
start_ok = false;
}
auto insert_result = unacked_events_.try_emplace(
event_id, EventInfo{request_uuid, render_process_id, version_id,
worker_thread_id, start_ok, external_request_result,
dispatch_start_time, dispatch_source,
lazy_background_active_on_dispatch, histogram_value});
DCHECK(insert_result.second) << "EventAckData: Duplicate event_id.";
if (dispatch_source == EventDispatchSource::kDispatchEventToProcess) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&EventAckData::EmitLateAckedEventTask,
weak_factory_.GetWeakPtr(), event_id),
kEventAckMetricTimeLimit);
}
}
void EventAckData::EmitLateAckedEventTask(int event_id) {
// If the event is still present then we haven't received the ack yet in
// `EventAckData::DecrementInflightEvent()`.
if (auto* value = base::FindOrNull(unacked_events_, event_id)) {
base::UmaHistogramBoolean(
"Extensions.Events.DidDispatchToAckSucceed.ExtensionServiceWorker3",
false);
EmitLateAckedEventTaskMetrics(*value);
}
}
// Emit metrics helpful in determining causes of `unacked_events_` that are not
// acked within the timeout.
void EventAckData::EmitLateAckedEventTaskMetrics(const EventInfo& event_info) {
base::UmaHistogramEnumeration(
"Extensions.Events.ServiceWorkerDispatchFailed.Event",
event_info.histogram_value, events::ENUM_BOUNDARY);
base::UmaHistogramBoolean(
"Extensions.Events.ServiceWorkerDispatchFailed.StartExternalRequestOk",
event_info.start_ok);
if (!event_info.start_ok) {
base::UmaHistogramEnumeration(
"Extensions.Events.ServiceWorkerDispatchFailed."
"StartExternalRequestResult",
event_info.external_request_result);
}
// TODO(crbug.com/40909770): Implement service worker running status as a late
// acked event metric when it can be more accurately determined. For example,
// it could be useful to determine if the late acked events are for already
// shut down workers and therefore wouldn't be "late".
}
// static
void EventAckData::EmitDispatchTimeMetrics(EventInfo& event_info) {
// Only emit events that use the EventRouter::DispatchEventToProcess() event
// routing flow since EventRouter::DispatchEventToSender() uses a different
// flow that doesn't include dispatch start and service worker start time.
if (event_info.dispatch_source ==
EventDispatchSource::kDispatchEventToProcess) {
base::UmaHistogramCustomMicrosecondsTimes(
"Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2",
/*sample=*/base::TimeTicks::Now() - event_info.dispatch_start_time,
/*min=*/base::Microseconds(1), /*max=*/base::Minutes(5),
/*buckets=*/100);
const char* active_metric_name =
event_info.lazy_background_active_on_dispatch
? "Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2."
"Active3"
: "Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2."
"Inactive3";
base::UmaHistogramCustomMicrosecondsTimes(
active_metric_name,
/*sample=*/base::TimeTicks::Now() - event_info.dispatch_start_time,
/*min=*/base::Microseconds(1), /*max=*/base::Minutes(5),
/*buckets=*/100);
base::UmaHistogramCustomTimes(
"Extensions.Events.DispatchToAckLongTime.ExtensionServiceWorker2",
/*sample=*/base::TimeTicks::Now() - event_info.dispatch_start_time,
/*min=*/base::Seconds(1), /*max=*/base::Days(1),
/*buckets=*/100);
// Emit only if we're within the expected event ack time limit. We'll take
// care of the emit for a late ack via a delayed task.
bool late_ack = (base::TimeTicks::Now() - event_info.dispatch_start_time) >
kEventAckMetricTimeLimit;
if (!late_ack) {
base::UmaHistogramBoolean(
"Extensions.Events.DidDispatchToAckSucceed.ExtensionServiceWorker3",
true);
}
}
}
void EventAckData::DecrementInflightEvent(
content::ServiceWorkerContext* context,
int render_process_id,
int64_t version_id,
int worker_thread_id,
int event_id,
bool worker_stopped,
base::OnceClosure failure_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Event not found.
auto request_info_iter = unacked_events_.find(event_id);
if (request_info_iter == unacked_events_.end()) {
if (worker_stopped) {
// If the worker has stopped, it's possible the event was already cleaned
// up by `ClearUnackedEventsForWorker` just before the ack arrived.
return;
}
// Event missing from a running worker. Bad ack.
std::move(failure_callback).Run();
return;
}
// Event found.
EventInfo& event_info = request_info_iter->second;
if (event_info.render_process_id != render_process_id ||
event_info.version_id != version_id ||
event_info.worker_thread_id != worker_thread_id) {
// Mismatched worker. This is always a bad ack, regardless of whether
// the sender is stopped or running.
std::move(failure_callback).Run();
return;
}
EmitDispatchTimeMetrics(event_info);
base::Uuid request_uuid = std::move(event_info.request_uuid);
bool start_ok = event_info.start_ok;
unacked_events_.erase(request_info_iter);
content::ServiceWorkerExternalRequestResult result =
context->FinishedExternalRequest(version_id, request_uuid);
base::UmaHistogramEnumeration(
"Extensions.ServiceWorkerBackground.FinishedExternalRequest_Result",
result);
// If the worker was already stopped or StartExternalRequest didn't succeed,
// the FinishedExternalRequest will legitimately fail.
if (worker_stopped || !start_ok) {
return;
}
base::UmaHistogramEnumeration(
"Extensions.ServiceWorkerBackground.FinishedExternalRequest_Result_"
"PostReturn",
result);
switch (result) {
case content::ServiceWorkerExternalRequestResult::kOk:
// Metrics have shown us that it is possible that a worker may not be found
// or not running at this point.
case content::ServiceWorkerExternalRequestResult::kWorkerNotFound:
case content::ServiceWorkerExternalRequestResult::kWorkerNotRunning:
// Null context can happen in the rare case if ServiceWorkerContextCore is
// torn down when EventRouter + BrowserContext are still alive and an
// event happens to be acked here.
case content::ServiceWorkerExternalRequestResult::kNullContext:
// TODO(crbug.com/41494056): Perform more graceful shutdown when
// ServiceWorkerContextCore is torn down.
// kBadRequestId can expectedly happen if a new instance of a worker starts
// while an ack for the previous worker is in-flight to the browser. We then
// receive the ack and ServiceWorkerContext can't find the
// external/in-flight request because the previous worker's
// `ServiceWorkerVersion` has been replaced by the new worker's
// `ServiceWorkerVersion`. The new version then does not have a record of
// the external/in-flight request and returns kBadRequestId.
case content::ServiceWorkerExternalRequestResult::kBadRequestId:
// TODO(crbug.com/40072982): Reliably detect when the above occurs and
// continue to not kill the renderer. But if the event is not for an old
// instance of the worker then consider CHECK()-ing since this could
// indicate a bug in the tracking of external requests in the browser.
break;
}
}
void EventAckData::ClearUnackedEventsForRenderProcess(int render_process_id) {
std::erase_if(unacked_events_, [render_process_id](const auto& entry) {
return entry.second.render_process_id == render_process_id;
});
}
void EventAckData::ClearUnackedEventsForWorker(
content::ServiceWorkerContext* context,
int render_process_id,
int64_t version_id,
int worker_thread_id) {
// Iterate manually because we need to access the EventInfo (to retrieve
// `request_uuid`) before erasing the element.
for (auto it = unacked_events_.begin(); it != unacked_events_.end();) {
const EventInfo& event_info = it->second;
if (event_info.render_process_id != render_process_id ||
event_info.version_id != version_id ||
event_info.worker_thread_id != worker_thread_id) {
it++;
continue;
}
// The worker stopped before acking the event. Notify SWContext that the
// external request finished. This balances the `StartingExternalRequest`
// call in `IncrementInflightEvent`.
if (context && event_info.start_ok) {
context->FinishedExternalRequest(event_info.version_id,
event_info.request_uuid);
}
it = unacked_events_.erase(it);
}
}
bool EventAckData::HasUnackedEventForTesting(int event_id) {
return base::FindOrNull(unacked_events_, event_id);
}
} // namespace extensions