// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/service_worker/service_worker_metrics.h"

#include <limits>
#include <string>

#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_client.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"

namespace content {

namespace {

const char* StartSituationToSuffix(
    ServiceWorkerMetrics::StartSituation situation) {
  // Don't change these returned strings. They are written (in hashed form) into
  // logs.
  switch (situation) {
    case ServiceWorkerMetrics::StartSituation::UNKNOWN:
      NOTREACHED();
    case ServiceWorkerMetrics::StartSituation::DURING_STARTUP:
      return ".DuringStartup";
    case ServiceWorkerMetrics::StartSituation::NEW_PROCESS:
      return ".NewProcess";
    case ServiceWorkerMetrics::StartSituation::EXISTING_UNREADY_PROCESS:
      return ".ExistingUnreadyProcess";
    case ServiceWorkerMetrics::StartSituation::EXISTING_READY_PROCESS:
      return ".ExistingReadyProcess";
  }
  NOTREACHED() << static_cast<int>(situation);
}

// TODO(falken): Remove this when the associated UMA are removed.
const char* StartSituationToDeprecatedSuffix(
    ServiceWorkerMetrics::StartSituation situation) {
  // Don't change this returned string. It is written (in hashed form) into
  // logs.
  switch (situation) {
    case ServiceWorkerMetrics::StartSituation::UNKNOWN:
      NOTREACHED();
    case ServiceWorkerMetrics::StartSituation::DURING_STARTUP:
      return "_DuringStartup";
    case ServiceWorkerMetrics::StartSituation::NEW_PROCESS:
      return "_NewProcess";
    case ServiceWorkerMetrics::StartSituation::EXISTING_UNREADY_PROCESS:
      return "_ExistingUnreadyProcess";
    case ServiceWorkerMetrics::StartSituation::EXISTING_READY_PROCESS:
      return "_ExistingReadyProcess";
  }
  NOTREACHED() << static_cast<int>(situation);
}

const char* EventTypeToSuffix(ServiceWorkerMetrics::EventType event_type) {
  // Don't change these returned strings. They are written (in hashed form) into
  // logs.
  switch (event_type) {
    case ServiceWorkerMetrics::EventType::ACTIVATE:
      return "_ACTIVATE";
    case ServiceWorkerMetrics::EventType::INSTALL:
      return "_INSTALL";
    case ServiceWorkerMetrics::EventType::SYNC:
      return "_SYNC";
    case ServiceWorkerMetrics::EventType::NOTIFICATION_CLICK:
      return "_NOTIFICATION_CLICK";
    case ServiceWorkerMetrics::EventType::PUSH:
      return "_PUSH";
    case ServiceWorkerMetrics::EventType::MESSAGE:
      return "_MESSAGE";
    case ServiceWorkerMetrics::EventType::NOTIFICATION_CLOSE:
      return "_NOTIFICATION_CLOSE";
    case ServiceWorkerMetrics::EventType::FETCH_MAIN_FRAME:
      return "_FETCH_MAIN_FRAME";
    case ServiceWorkerMetrics::EventType::FETCH_SUB_FRAME:
      return "_FETCH_SUB_FRAME";
    case ServiceWorkerMetrics::EventType::FETCH_SHARED_WORKER:
      return "_FETCH_SHARED_WORKER";
    case ServiceWorkerMetrics::EventType::FETCH_SUB_RESOURCE:
      return "_FETCH_SUB_RESOURCE";
    case ServiceWorkerMetrics::EventType::UNKNOWN:
      return "_UNKNOWN";
    case ServiceWorkerMetrics::EventType::FETCH_WAITUNTIL:
      return "_FETCH_WAITUNTIL";
    case ServiceWorkerMetrics::EventType::EXTERNAL_REQUEST:
      return "_EXTERNAL_REQUEST";
    case ServiceWorkerMetrics::EventType::PAYMENT_REQUEST:
      return "_PAYMENT_REQUEST";
    case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_ABORT:
      return "_BACKGROUND_FETCH_ABORT";
    case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_CLICK:
      return "_BACKGROUND_FETCH_CLICK";
    case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_FAIL:
      return "_BACKGROUND_FETCH_FAIL";
    case ServiceWorkerMetrics::EventType::NAVIGATION_HINT:
      return "_NAVIGATION_HINT";
    case ServiceWorkerMetrics::EventType::CAN_MAKE_PAYMENT:
      return "_CAN_MAKE_PAYMENT";
    case ServiceWorkerMetrics::EventType::ABORT_PAYMENT:
      return "_ABORT_PAYMENT";
    case ServiceWorkerMetrics::EventType::COOKIE_CHANGE:
      return "_COOKIE_CHANGE";
    case ServiceWorkerMetrics::EventType::BACKGROUND_FETCH_SUCCESS:
      return "_BACKGROUND_FETCH_SUCCESS";
    case ServiceWorkerMetrics::EventType::PERIODIC_SYNC:
      return "_PERIODIC_SYNC";
    case ServiceWorkerMetrics::EventType::CONTENT_DELETE:
      return "_CONTENT_DELETE";
    case ServiceWorkerMetrics::EventType::PUSH_SUBSCRIPTION_CHANGE:
      return "_PUSH_SUBSCRIPTION_CHANGE";
    case ServiceWorkerMetrics::EventType::FETCH_FENCED_FRAME:
      return "_FETCH_FENCED_FRAME";
    case ServiceWorkerMetrics::EventType::BYPASS_MAIN_RESOURCE:
      return "_BYPASS_MAIN_RESOURCE";
    case ServiceWorkerMetrics::EventType::SKIP_EMPTY_FETCH_HANDLER:
      return "_SKIP_EMPTY_FETCH_HANDLER";
    case ServiceWorkerMetrics::EventType::
        BYPASS_ONLY_IF_SERVICE_WORKER_NOT_STARTED:
      return "_BYPASS_ONLY_IF_SERVICE_WORKER_NOT_STARTED";
    case ServiceWorkerMetrics::EventType::WARM_UP:
      return "_WARM_UP";
    case ServiceWorkerMetrics::EventType::STATIC_ROUTER:
      return "_STATIC_ROUTING";
  }
  return "_UNKNOWN";
}

}  // namespace

const char* ServiceWorkerMetrics::EventTypeToString(EventType event_type) {
  switch (event_type) {
    case EventType::ACTIVATE:
      return "Activate";
    case EventType::INSTALL:
      return "Install";
    case EventType::SYNC:
      return "Sync";
    case EventType::NOTIFICATION_CLICK:
      return "Notification Click";
    case EventType::NOTIFICATION_CLOSE:
      return "Notification Close";
    case EventType::PUSH:
      return "Push";
    case EventType::MESSAGE:
      return "Message";
    case EventType::FETCH_MAIN_FRAME:
      return "Fetch Main Frame";
    case EventType::FETCH_SUB_FRAME:
      return "Fetch Sub Frame";
    case EventType::FETCH_SHARED_WORKER:
      return "Fetch Shared Worker";
    case EventType::FETCH_SUB_RESOURCE:
      return "Fetch Subresource";
    case EventType::UNKNOWN:
      return "Unknown";
    case EventType::FETCH_WAITUNTIL:
      return "Fetch WaitUntil";
    case EventType::EXTERNAL_REQUEST:
      return "External Request";
    case EventType::PAYMENT_REQUEST:
      return "Payment Request";
    case EventType::BACKGROUND_FETCH_ABORT:
      return "Background Fetch Abort";
    case EventType::BACKGROUND_FETCH_CLICK:
      return "Background Fetch Click";
    case EventType::BACKGROUND_FETCH_FAIL:
      return "Background Fetch Fail";
    case EventType::NAVIGATION_HINT:
      return "Navigation Hint";
    case EventType::CAN_MAKE_PAYMENT:
      return "Can Make Payment";
    case EventType::ABORT_PAYMENT:
      return "Abort Payment";
    case EventType::COOKIE_CHANGE:
      return "Cookie Change";
    case EventType::BACKGROUND_FETCH_SUCCESS:
      return "Background Fetch Success";
    case EventType::PERIODIC_SYNC:
      return "Periodic Sync";
    case EventType::CONTENT_DELETE:
      return "Content Delete";
    case EventType::PUSH_SUBSCRIPTION_CHANGE:
      return "Push Subscription Change";
    case EventType::FETCH_FENCED_FRAME:
      return "Fetch Fenced Frame";
    case ServiceWorkerMetrics::EventType::BYPASS_MAIN_RESOURCE:
      return "_BYPASS_MAIN_RESOURCE";
    case ServiceWorkerMetrics::EventType::SKIP_EMPTY_FETCH_HANDLER:
      return "Skip Empty Fetch Handler";
    case ServiceWorkerMetrics::EventType::
        BYPASS_ONLY_IF_SERVICE_WORKER_NOT_STARTED:
      return "Bypass Only If ServiceWorker Is Not Started";
    case ServiceWorkerMetrics::EventType::WARM_UP:
      return "Warm Up";
    case ServiceWorkerMetrics::EventType::STATIC_ROUTER:
      return "Static Routing";
  }
  NOTREACHED() << "Got unexpected event type: " << static_cast<int>(event_type);
}

const char* ServiceWorkerMetrics::StartSituationToString(
    StartSituation start_situation) {
  switch (start_situation) {
    case StartSituation::UNKNOWN:
      return "Unknown";
    case StartSituation::DURING_STARTUP:
      return "During startup";
    case StartSituation::NEW_PROCESS:
      return "New process";
    case StartSituation::EXISTING_UNREADY_PROCESS:
      return "Existing unready process";
    case StartSituation::EXISTING_READY_PROCESS:
      return "Existing ready process";
  }
  NOTREACHED() << "Got unexpected start situation: "
               << static_cast<int>(start_situation);
}

void ServiceWorkerMetrics::CountReadResponseResult(
    ServiceWorkerMetrics::ReadResponseResult result) {
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.DiskCache.ReadResponseResult",
                            result, NUM_READ_RESPONSE_RESULT_TYPES);
}

void ServiceWorkerMetrics::CountWriteResponseResult(
    ServiceWorkerMetrics::WriteResponseResult result) {
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.DiskCache.WriteResponseResult",
                            result, NUM_WRITE_RESPONSE_RESULT_TYPES);
}

void ServiceWorkerMetrics::RecordStartInstalledWorkerStatus(
    blink::ServiceWorkerStatusCode status,
    EventType purpose) {
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.StartWorker.Status", status);
  base::UmaHistogramEnumeration(
      base::StrCat({"ServiceWorker.StartWorker.StatusByPurpose",
                    EventTypeToSuffix(purpose)}),
      status);
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.StartWorker.Purpose", purpose);
  if (status == blink::ServiceWorkerStatusCode::kErrorTimeout) {
    UMA_HISTOGRAM_ENUMERATION("ServiceWorker.StartWorker.Timeout.StartPurpose",
                              purpose);
  }
}

void ServiceWorkerMetrics::RecordRunAfterStartWorkerStatus(
    blink::EmbeddedWorkerStatus running_status,
    EventType purpose) {
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.MaybeStartWorker.RunningStatus",
                            running_status);
  base::UmaHistogramEnumeration(
      base::StrCat({"ServiceWorker.MaybeStartWorker.RunningStatusByPurpose",
                    EventTypeToSuffix(purpose)}),
      running_status);
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.MaybeStartWorker.Purpose", purpose);
}

void ServiceWorkerMetrics::RecordStartWorkerTime(base::TimeDelta time,
                                                 bool is_installed,
                                                 StartSituation start_situation,
                                                 EventType purpose) {
  if (is_installed) {
    DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES("ServiceWorker.StartWorker.Time",
                                          time);
    base::UmaHistogramMediumTimes(
        base::StrCat({"ServiceWorker.StartWorker.Time",
                      StartSituationToDeprecatedSuffix(start_situation)}),
        time);
    base::UmaHistogramMediumTimes(
        base::StrCat({"ServiceWorker.StartWorker.Time",
                      StartSituationToDeprecatedSuffix(start_situation),
                      EventTypeToSuffix(purpose)}),
        time);
    base::UmaHistogramMediumTimes(
        base::StrCat(
            {"ServiceWorker.StartWorker.Time_Any", EventTypeToSuffix(purpose)}),
        time);
  } else {
    DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES("ServiceWorker.StartNewWorker.Time",
                                          time);
  }
}

void ServiceWorkerMetrics::RecordActivateEventStatus(
    blink::ServiceWorkerStatusCode status,
    bool is_shutdown) {
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.ActivateEventStatus", status);
  if (is_shutdown) {
    UMA_HISTOGRAM_ENUMERATION("ServiceWorker.ActivateEventStatus_InShutdown",
                              status);
  } else {
    UMA_HISTOGRAM_ENUMERATION("ServiceWorker.ActivateEventStatus_NotInShutdown",
                              status);
  }
}

void ServiceWorkerMetrics::RecordInstallEventStatus(
    blink::ServiceWorkerStatusCode status,
    uint32_t fetch_count) {
  base::UmaHistogramEnumeration("ServiceWorker.InstallEvent.All.Status",
                                status);
  base::UmaHistogramCounts1000("ServiceWorker.InstallEvent.All.FetchCount",
                               fetch_count);
  if (fetch_count > 0) {
    base::UmaHistogramEnumeration("ServiceWorker.InstallEvent.WithFetch.Status",
                                  status);
  }
}

void ServiceWorkerMetrics::RecordEventDuration(EventType event,
                                               base::TimeDelta time,
                                               bool was_handled,
                                               uint32_t fetch_count) {
  switch (event) {
    case EventType::ACTIVATE:
      base::UmaHistogramMediumTimes("ServiceWorker.ActivateEvent.Time2", time);
      break;
    case EventType::INSTALL:
      base::UmaHistogramMediumTimes("ServiceWorker.InstallEvent.All.Time",
                                    time);
      if (fetch_count) {
        base::UmaHistogramMediumTimes(
            "ServiceWorker.InstallEvent.WithFetch.Time", time);
      }
      break;
    case EventType::MESSAGE:
      DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
          "ServiceWorker.ExtendableMessageEvent.Time", time);
      break;
    case EventType::FETCH_MAIN_FRAME:
    case EventType::FETCH_SUB_FRAME:
    case EventType::FETCH_SHARED_WORKER:
    case EventType::FETCH_SUB_RESOURCE:
    case EventType::FETCH_FENCED_FRAME:
      if (was_handled) {
        DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
            "ServiceWorker.FetchEvent.HasResponse.Time", time);
      } else {
        DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
            "ServiceWorker.FetchEvent.Fallback.Time", time);
      }
      break;
    case EventType::PAYMENT_REQUEST:
      DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
          "ServiceWorker.PaymentRequestEvent.Time", time);
      break;
    case EventType::PERIODIC_SYNC:
      DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
          "ServiceWorker.PeriodicBackgroundSyncEvent.Time", time);
      break;
    case EventType::SYNC:
    case EventType::NOTIFICATION_CLICK:
    case EventType::PUSH:
    case EventType::NOTIFICATION_CLOSE:
    case EventType::FETCH_WAITUNTIL:
    case EventType::EXTERNAL_REQUEST:
    case EventType::BACKGROUND_FETCH_ABORT:
    case EventType::BACKGROUND_FETCH_CLICK:
    case EventType::BACKGROUND_FETCH_FAIL:
    case EventType::CAN_MAKE_PAYMENT:
    case EventType::ABORT_PAYMENT:
    case EventType::COOKIE_CHANGE:
    case EventType::BACKGROUND_FETCH_SUCCESS:
    case EventType::CONTENT_DELETE:
    case EventType::PUSH_SUBSCRIPTION_CHANGE:
    case EventType::WARM_UP:
      // Do nothing: the warm up should not be sent as an event.
      break;
    case EventType::NAVIGATION_HINT:
    // The navigation hint should not be sent as an event.
    case EventType::BYPASS_MAIN_RESOURCE:
    // The bypass main resource should not be sent as an event.
    case EventType::SKIP_EMPTY_FETCH_HANDLER:
    // The skip empty fetch handler should not be sent as an event.
    case EventType::BYPASS_ONLY_IF_SERVICE_WORKER_NOT_STARTED:
    // The bypass_only_if_service_worker_not_started should not be sent as an
    // event.
    case EventType::STATIC_ROUTER:
    // Static Routing should not be sent as an event.
    case EventType::UNKNOWN:
      NOTREACHED() << "Invalid event type";
  }
}

void ServiceWorkerMetrics::RecordFetchEventStatus(
    bool is_main_resource,
    blink::ServiceWorkerStatusCode status) {
  if (is_main_resource) {
    UMA_HISTOGRAM_ENUMERATION("ServiceWorker.FetchEvent.MainResource.Status",
                              status);
  } else {
    UMA_HISTOGRAM_ENUMERATION("ServiceWorker.FetchEvent.Subresource.Status",
                              status);
  }
}

void ServiceWorkerMetrics::RecordStartWorkerTiming(const StartTimes& times,
                                                   StartSituation situation) {
  // Bail if the timings across processes weren't consistent.
  if (!base::TimeTicks::IsHighResolution() ||
      !base::TimeTicks::IsConsistentAcrossProcesses()) {
    RecordStartWorkerTimingClockConsistency(
        CrossProcessTimeDelta::INACCURATE_CLOCK);
    return;
  }
  if (times.remote_start_worker_received < times.local_start_worker_sent ||
      times.local_end < times.remote_script_evaluation_end) {
    RecordStartWorkerTimingClockConsistency(CrossProcessTimeDelta::NEGATIVE);
    return;
  }
  RecordStartWorkerTimingClockConsistency(CrossProcessTimeDelta::NORMAL);

  // Total duration.
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES("ServiceWorker.StartTiming.Duration",
                                        times.local_end - times.local_start);
  base::UmaHistogramMediumTimes(
      base::StrCat({"ServiceWorker.StartTiming.Duration",
                    StartSituationToSuffix(situation)}),
      times.local_end - times.local_start);

  // SentStartWorker milestone.
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.StartToSentStartWorker",
      times.local_start_worker_sent - times.local_start);

  // ReceivedStartWorker milestone.
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.StartToReceivedStartWorker",
      times.remote_start_worker_received - times.local_start);
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.SentStartWorkerToReceivedStartWorker",
      times.remote_start_worker_received - times.local_start_worker_sent);

  // ScriptEvaluationStart milestone.
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.StartToScriptEvaluationStart",
      times.remote_script_evaluation_start - times.local_start);
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.ReceivedStartWorkerToScriptEvaluationStart",
      times.remote_script_evaluation_start -
          times.remote_start_worker_received);

  // ScriptEvaluationEnd milestone.
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.StartToScriptEvaluationEnd",
      times.remote_script_evaluation_end - times.local_start);
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.ScriptEvaluationStartToScriptEvaluationEnd",
      times.remote_script_evaluation_end -
          times.remote_script_evaluation_start);

  // End milestone.
  DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES(
      "ServiceWorker.StartTiming.ScriptEvaluationEndToEnd",
      times.local_end - times.remote_script_evaluation_end);
}

void ServiceWorkerMetrics::RecordStartWorkerTimingClockConsistency(
    CrossProcessTimeDelta type) {
  UMA_HISTOGRAM_ENUMERATION("ServiceWorker.StartTiming.ClockConsistency", type);
}

void ServiceWorkerMetrics::RecordSkipServiceWorkerOnNavigation(
    bool skip_service_worker) {
  static bool is_first_call = true;
  if (is_first_call) {
    is_first_call = false;
    if (!GetContentClient()->browser()->IsBrowserStartupComplete()) {
      base::UmaHistogramBoolean(
          "ServiceWorker.OnBrowserStartup.SkipServiceWorkerOnFirstNavigation",
          skip_service_worker);
    }
  } else {
    if (GetContentClient()->browser()->IsBrowserStartupComplete()) {
      base::UmaHistogramBoolean(
          "ServiceWorker.SkipCallingFindRegistrationForClientUrl",
          skip_service_worker);
    }
  }
}

void ServiceWorkerMetrics::RecordFindRegistrationForClientUrlTime(
    base::TimeDelta time) {
  static bool is_first_call = true;
  if (is_first_call) {
    is_first_call = false;
    if (!GetContentClient()->browser()->IsBrowserStartupComplete()) {
      base::UmaHistogramMediumTimes(
          "ServiceWorker.OnBrowserStartup.FirstFindRegistrationForClientUrl."
          "Time",
          time);
    }
  } else {
    if (GetContentClient()->browser()->IsBrowserStartupComplete()) {
      base::UmaHistogramMediumTimes(
          "ServiceWorker.FindRegistrationForClientUrl.Time", time);
    }
  }
}

void ServiceWorkerMetrics::RecordMainScriptRequestValidationResult(
    ServiceWorkerMainScriptRequestValidationResult result) {
  base::UmaHistogramEnumeration("ServiceWorker.MainScriptUrlValidationResult",
                                result);
}

void ServiceWorkerMetrics::RecordMessageDispatchContextValidationResult(
    ServiceWorkerMessageDispatchContextValidationResult result) {
  base::UmaHistogramEnumeration(
      "ServiceWorker.MessageDispatch.ContextValidation", result);
}

void ServiceWorkerMetrics::RecordStartWorkerContextValidationDifference(
    ServiceWorkerStartWorkerContextValidationDifference result) {
  base::UmaHistogramEnumeration(
      "ServiceWorker.StartWorker.ContextValidationDifference", result);
}

}  // namespace content
