chromium/chrome/browser/nearby_sharing/nearby_share_transfer_profiler.cc

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

#include "chrome/browser/nearby_sharing/nearby_share_transfer_profiler.h"

#include "base/time/time.h"
#include "chrome/browser/nearby_sharing/nearby_share_metrics.h"
#include "components/cross_device/logging/logging.h"

base::TimeDelta ComputeDelta(const base::TimeTicks& start,
                             const base::TimeTicks& end) {
  // The delta between start and end times must be non-negative.
  // Note: Due to how time works in unittests, they may be equal.
  CHECK(end >= start);
  return end - start;
}

NearbyShareTransferProfiler::NearbyShareTransferProfiler() {}
NearbyShareTransferProfiler::~NearbyShareTransferProfiler() {}

void NearbyShareTransferProfiler::OnEndpointDiscovered(
    const std::string& endpoint_id) {
  // It is possible for an endpoint to be rediscovered before it is lost, so
  // reset any existing state associated with it. The can easily occur when a
  // transfer is requested, the dialog is closed after the endpoint is
  // discovered, and then a new transfer is requested before the endpoint is
  // lost.
  if (sender_data_.contains(endpoint_id)) {
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id << "] rediscovered";
    sender_data_.erase(endpoint_id);
  }

  // There should not be data associated with an endpoint before it is
  // discovered.
  CHECK(!sender_data_.contains(endpoint_id));
  sender_data_[endpoint_id].discovered_time = base::TimeTicks::Now();
}

void NearbyShareTransferProfiler::OnEndpointLost(
    const std::string& endpoint_id) {
  // The endpoint must have been discovered before it can be lost.
  CHECK(sender_data_.contains(endpoint_id));
  CHECK(sender_data_[endpoint_id].discovered_time.has_value());
  sender_data_.erase(endpoint_id);
}

void NearbyShareTransferProfiler::OnOutgoingEndpointDecoded(
    const std::string& endpoint_id) {
  // The endpoint must have been discovered before it can be decoded.
  CHECK(sender_data_.contains(endpoint_id));
  CHECK(sender_data_[endpoint_id].discovered_time.has_value());
  // The endpoint should not have been connected yet, since it must be decoded
  // in order to connect.
  CHECK(!sender_data_[endpoint_id].connection_established_time.has_value());

  // An endpoint should only be decoded once.
  CHECK(!sender_data_[endpoint_id].endpoint_decoded_time.has_value());
  sender_data_[endpoint_id].endpoint_decoded_time = base::TimeTicks::Now();
}

void NearbyShareTransferProfiler::OnShareTargetSelected(
    const std::string& endpoint_id) {
  // The endpoint must have been discovered and decoded before it can be
  // selected. Note: A connection may have been established prior, in the case
  // where the same endpoint is used for multiple transfers without redisovery.
  CHECK(sender_data_.contains(endpoint_id));
  CHECK(sender_data_[endpoint_id].discovered_time.has_value());

  // An endpoint should only be selected once.
  CHECK(!sender_data_[endpoint_id].share_target_selected_time.has_value());
  sender_data_[endpoint_id].share_target_selected_time = base::TimeTicks::Now();
}

void NearbyShareTransferProfiler::OnConnectionEstablished(
    const std::string& endpoint_id) {
  // The endpoint must have been discovered, decoded, and selected before a
  // connection can be established.
  CHECK(sender_data_.contains(endpoint_id));
  CHECK(sender_data_[endpoint_id].discovered_time.has_value());
  CHECK(sender_data_[endpoint_id].endpoint_decoded_time.has_value());
  CHECK(sender_data_[endpoint_id].share_target_selected_time.has_value());

  // Note: the same receiver can be reused for subsequent shares without
  // triggering the discovery logic.
  bool is_first_connection =
      !sender_data_[endpoint_id].connection_established_time.has_value();
  sender_data_[endpoint_id].connection_established_time =
      base::TimeTicks::Now();

  // Only log the time to first connection, since subsequent connections without
  // rediscovery could dramatically pollute these metrics.
  if (is_first_connection) {
    // Compute and log the time delta between when the endpoint was first
    // discovered and when the connection was established. This provides insight
    // into how long it takes for a sender to detect a receiver, select it, and
    // connect to it. Note: this delta includes user interaction, so it may vary
    // significantly for each connection.
    base::TimeDelta discovery_delta = ComputeDelta(
        sender_data_[endpoint_id].discovered_time.value(),
        sender_data_[endpoint_id].connection_established_time.value());
    RecordNearbyShareDiscoveredToConnectionEstablishedDuration(discovery_delta);
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id
        << "] discovery to first connection established: " << discovery_delta;
  }
}

void NearbyShareTransferProfiler::OnIntroductionFrameSent(
    const std::string& endpoint_id) {
  // The endpoint must have been discovered, decoded, selected, and connected
  // before an introduction frame can be sent.
  if (!sender_data_.contains(endpoint_id) ||
      !sender_data_[endpoint_id].discovered_time.has_value() ||
      !sender_data_[endpoint_id].endpoint_decoded_time.has_value() ||
      !sender_data_[endpoint_id].share_target_selected_time.has_value() ||
      !sender_data_[endpoint_id].connection_established_time.has_value()) {
    return;
  }

  // Note: the same receiver can be reused for subsequent shares without
  // triggering the discovery logic.
  bool is_first_introduction =
      !sender_data_[endpoint_id].introduction_sent_time.has_value();
  sender_data_[endpoint_id].introduction_sent_time = base::TimeTicks::Now();

  // Only log the time to first introduction, since subsequent introductions
  // without rediscovery could dramatically pollute these metrics.
  if (is_first_introduction) {
    // Compute and log the time delta between when the outgoing endpoint was
    // decoded and when the introduction frame was sent. This provides insight
    // into how long it takes to notify the receiver that it has been selected.
    // Note: this delta includes user interaction, so it may vary significantly
    // for each transfer.
    base::TimeDelta introduction_delta =
        ComputeDelta(sender_data_[endpoint_id].endpoint_decoded_time.value(),
                     sender_data_[endpoint_id].introduction_sent_time.value());
    // We do not log this to UMA currently, since the user interaction timing
    // will make it unacceptably noisy.
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id
        << "] decode to introduction sent: " << introduction_delta;
  }
  // Compute and log the time delta between when the user selects the endpoint
  // as a share target and when the introduction frame was sent. This provides
  // insight into how long it takes to establish a connection and notify the
  // receiver.
  base::TimeDelta selection_delta =
      ComputeDelta(sender_data_[endpoint_id].share_target_selected_time.value(),
                   sender_data_[endpoint_id].introduction_sent_time.value());
  RecordNearbyShareInitiatedToSentIntroductionFrameDuration(selection_delta);
  CD_LOG(INFO, Feature::NS)
      << "Endpoint [" << endpoint_id
      << "] share target selected to introduction sent: " << selection_delta;
}

void NearbyShareTransferProfiler::OnSendStart(const std::string& endpoint_id) {
  // The endpoint must have been discovered, decoded, selected, connected, and
  // introduced before transfers can be started.
  if (!sender_data_.contains(endpoint_id) ||
      !sender_data_[endpoint_id].discovered_time.has_value() ||
      !sender_data_[endpoint_id].endpoint_decoded_time.has_value() ||
      !sender_data_[endpoint_id].share_target_selected_time.has_value() ||
      !sender_data_[endpoint_id].connection_established_time.has_value() ||
      !sender_data_[endpoint_id].introduction_sent_time.has_value()) {
    return;
  }

  // Transfers should only be started once.
  if (sender_data_[endpoint_id].send_start_time.has_value()) {
    return;
  }
  sender_data_[endpoint_id].send_start_time = base::TimeTicks::Now();
}

void NearbyShareTransferProfiler::OnSendComplete(
    const std::string& endpoint_id,
    TransferMetadata::Status status) {
  // Only log completion metrics for successful transfers.
  if (status == TransferMetadata::Status::kComplete) {
    // The endpoint must have been discovered, decoded, selected, connected,
    // introduced, and the sending must have started before a transfer can
    // successfully complete.
    CHECK(sender_data_.contains(endpoint_id));
    CHECK(sender_data_[endpoint_id].discovered_time.has_value());
    CHECK(sender_data_[endpoint_id].endpoint_decoded_time.has_value());
    CHECK(sender_data_[endpoint_id].share_target_selected_time.has_value());
    CHECK(sender_data_[endpoint_id].connection_established_time.has_value());
    CHECK(sender_data_[endpoint_id].introduction_sent_time.has_value());
    CHECK(sender_data_[endpoint_id].send_start_time.has_value());

    base::TimeTicks now = base::TimeTicks::Now();

    // A bandwidth upgrade is not guaranteed to have been successful before the
    // transfer is complete.
    bool has_bandwidth_upgraded =
        sender_data_[endpoint_id].bandwidth_upgrade_time.has_value();
    if (has_bandwidth_upgraded) {
      // There should be an upgraded medium recoded.
      CHECK(sender_data_[endpoint_id].upgraded_medium.has_value());

      // Compute and log the time delta between when the first bandwidth upgrade
      // completes and when transfer finishes. This provides insight into how
      // long the transfer takes to complete for a given transfer medium.
      base::TimeDelta upgrade_delta = ComputeDelta(
          sender_data_[endpoint_id].bandwidth_upgrade_time.value(), now);
      RecordNearbyShareBandwidthUpgradeToAllFilesSentDuration(
          sender_data_[endpoint_id].upgraded_medium.value(), upgrade_delta);
      CD_LOG(INFO, Feature::NS)
          << "Endpoint [" << endpoint_id << "] bandwidth upgrade ("
          << sender_data_[endpoint_id].upgraded_medium.value()
          << ") to transfer complete: " << upgrade_delta;
    }

    // Compute and log the time delta between when the sender starts sending
    // data and when transfer finishes. This provides insight into the time a
    // transfer takes to complete.
    base::TimeDelta completion_delta =
        ComputeDelta(sender_data_[endpoint_id].send_start_time.value(), now);
    RecordNearbyShareStartSendFilesToAllFilesSentDuration(completion_delta);
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id
        << "] file sent to transfer complete: " << completion_delta;

    // Compute and log the time delta between when the user has selected a
    // receiver and when transfer finishes. This provides insight into the
    // perceived user time the nearby share process takes to complete for the
    // sender. Note: this delta includes user interaction (the receiver needs to
    // accept the transfer), so it may vary significantly for each transfer.
    base::TimeDelta total_delta = ComputeDelta(
        sender_data_[endpoint_id].share_target_selected_time.value(), now);
    RecordNearbyShareInitiatedToAllFilesSentDuration(total_delta);
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id
        << "] share target selected to transfer complete: " << total_delta;
  }

  // Reset the endpoint using the current discovery, decode, connection, and
  // introduction timestamps since they could be reused for subsequent
  // transfers.
  sender_data_[endpoint_id] = SenderData{
      .discovered_time = sender_data_[endpoint_id].discovered_time,
      .endpoint_decoded_time = sender_data_[endpoint_id].endpoint_decoded_time,
      .connection_established_time =
          sender_data_[endpoint_id].connection_established_time,
      .introduction_sent_time =
          sender_data_[endpoint_id].introduction_sent_time,
  };
}

void NearbyShareTransferProfiler::OnIncomingEndpointDecoded(
    const std::string& endpoint_id,
    bool is_high_visibility) {
  // An endpoint should only be decoded once.
  if (receiver_data_.contains(endpoint_id)) {
    return;
  }
  receiver_data_[endpoint_id].endpoint_decoded_time = base::TimeTicks::Now();

  // High-visibility endpoints emit different metrics, so save this for later
  // use.
  if (is_high_visibility) {
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id << "] in high-visibility mode";
    receiver_data_[endpoint_id].is_high_visibility = true;
  }
}

void NearbyShareTransferProfiler::OnPairedKeyHandshakeComplete(
    const std::string& endpoint_id) {
  // The endpoint must have been decoded before a handshake can be complete.
  CHECK(receiver_data_.contains(endpoint_id));
  CHECK(receiver_data_[endpoint_id].endpoint_decoded_time.has_value());

  // Only one handshake should be completed for each transfer.
  CHECK(!receiver_data_[endpoint_id].paired_key_handshake_time.has_value());
  receiver_data_[endpoint_id].paired_key_handshake_time =
      base::TimeTicks::Now();
}

void NearbyShareTransferProfiler::OnIntroductionFrameReceived(
    const std::string& endpoint_id) {
  // The endpoint must have been decoded and the handshake completed before an
  // introduction frame can be received.
  CHECK(receiver_data_.contains(endpoint_id));
  CHECK(receiver_data_[endpoint_id].endpoint_decoded_time.has_value());
  CHECK(receiver_data_[endpoint_id].paired_key_handshake_time.has_value());

  // Only one introduction frame should be received.
  CHECK(!receiver_data_[endpoint_id].introduction_received_time.has_value());
  receiver_data_[endpoint_id].introduction_received_time =
      base::TimeTicks::Now();

  // Compute and log the time delta between when the receiver decodes the
  // incoming endpoint and when the introduction frame was received. This
  // provides insight into how long the paired key handshake takes to complete
  // and be acknowledged.
  base::TimeDelta introduction_delta = ComputeDelta(
      receiver_data_[endpoint_id].endpoint_decoded_time.value(),
      receiver_data_[endpoint_id].introduction_received_time.value());
  RecordNearbyShareEndpointDecodedToReceivedIntroductionFrameDuration(
      introduction_delta);
  CD_LOG(INFO, Feature::NS)
      << "Endpoint [" << endpoint_id
      << "] decode to introduction received: " << introduction_delta;
}
void NearbyShareTransferProfiler::OnTransferAccepted(
    const std::string& endpoint_id) {
  // The endpoint must have been decoded and introduced before a transfer can be
  // accepted.
  if (!receiver_data_.contains(endpoint_id) ||
      !receiver_data_[endpoint_id].endpoint_decoded_time.has_value() ||
      !receiver_data_[endpoint_id].introduction_received_time.has_value()) {
    return;
  }

  // A transfer can only be accepted once.
  if (receiver_data_[endpoint_id].accept_transfer_time.has_value()) {
    return;
  }
  receiver_data_[endpoint_id].accept_transfer_time = base::TimeTicks::Now();
}

void NearbyShareTransferProfiler::OnReceiveComplete(
    const std::string& endpoint_id,
    TransferMetadata::Status status) {
  // Only log completion metrics for successful transfers.
  if (status == TransferMetadata::Status::kComplete) {
    // The endpoint must have been decoded, introduced, and accepted before a
    // transfer can be completed successfully.
    CHECK(receiver_data_.contains(endpoint_id));
    CHECK(receiver_data_[endpoint_id].endpoint_decoded_time.has_value());
    CHECK(receiver_data_[endpoint_id].introduction_received_time.has_value());
    CHECK(receiver_data_[endpoint_id].accept_transfer_time.has_value());

    base::TimeTicks now = base::TimeTicks::Now();

    // A bandwidth upgrade is not guaranteed to have been successful before the
    // transfer is complete.
    bool has_bandwidth_upgraded =
        receiver_data_[endpoint_id].bandwidth_upgrade_time.has_value();
    if (has_bandwidth_upgraded) {
      // There should be an upgraded medium recoded.
      CHECK(receiver_data_[endpoint_id].upgraded_medium.has_value());

      // Compute and log the time delta between when the first bandwidth upgrade
      // completes and when the transfer finishes. This provides insight into
      // how long the transfer takes to complete for a given transfer medium.
      base::TimeDelta upgrade_delta = ComputeDelta(
          receiver_data_[endpoint_id].bandwidth_upgrade_time.value(), now);
      RecordNearbyShareBandwidthUpgradeToAllFilesReceivedDuration(
          receiver_data_[endpoint_id].upgraded_medium.value(), upgrade_delta);
      CD_LOG(INFO, Feature::NS)
          << "Endpoint [" << endpoint_id << "] bandwidth upgrade ("
          << receiver_data_[endpoint_id].upgraded_medium.value()
          << ") to transfer complete: " << upgrade_delta;
    }

    // Compute and log the time delta between when the user accepts the transfer
    // and when the transfer finishes. This provides insight into the time a
    // transfer takes to complete.
    base::TimeDelta completion_delta = ComputeDelta(
        receiver_data_[endpoint_id].accept_transfer_time.value(), now);
    RecordNearbyShareAcceptedTransferToAllFilesReceivedDuration(
        completion_delta);
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id
        << "] transfer accepted to transfer complete: " << completion_delta;

    // Compute and log the time delta between when the receiver has verified the
    // connection and when the transfer finishes. This provides insight into the
    // perceived user time the entire nearby share process takes to complete for
    // the receiver.
    base::TimeDelta total_delta = ComputeDelta(
        receiver_data_[endpoint_id].introduction_received_time.value(), now);
    RecordNearbyShareReceivedIntroductionFrameToAllFilesReceivedDuration(
        total_delta);
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id
        << "] introduction to transfer complete: " << total_delta;
  }

  // Reset the endpoint since the transfer is complete.
  receiver_data_.erase(endpoint_id);
}

void NearbyShareTransferProfiler::OnBandwidthUpgrade(
    const std::string& endpoint_id,
    nearby::connections::mojom::Medium medium) {
  // An endpoint must either be a sender or a receiver.
  bool is_sender = sender_data_.contains(endpoint_id);
  bool is_receiver = receiver_data_.contains(endpoint_id);

  if (!is_sender && !is_receiver) {
    // It is possible for the |endpoint_id| to be in neither |sender_data_| nor
    // |receiver_data_| if, e.g. the transfer is cancelled just before the
    // bandwidth upgrade completes.
    return;
  }

  // An endpoint cannot be both a sender and a receiver.
  if (is_sender && is_receiver) {
    return;
  }

  if (is_sender) {
    // A connection must have been established at this point.
    if (!sender_data_[endpoint_id].connection_established_time.has_value()) {
      return;
    }

    // Bandwidth upgrades are requested a number of times, so it is possible for
    // more than one upgrade to complete successfully.
    bool is_first_upgrade =
        !sender_data_[endpoint_id].bandwidth_upgrade_time.has_value();
    sender_data_[endpoint_id].upgraded_medium = medium;
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id << "] upgraded bandwidth to " << medium;

    // Only log metrics the first time bandwidth upgrade is successful, since
    // subsequent upgrades could pollute these metrics.
    if (is_first_upgrade) {
      // Only store the first time a bandwidth upgrade occurred.
      sender_data_[endpoint_id].bandwidth_upgrade_time = base::TimeTicks::Now();

      // Compute and log the time delta between when the sender establishes a
      // connection and when the first bandwidth upgrade occurs. This provides
      // insight into how long a bandwidth upgrade takes to complete from the
      // sender side.
      base::TimeDelta upgrade_delta = ComputeDelta(
          sender_data_[endpoint_id].connection_established_time.value(),
          sender_data_[endpoint_id].bandwidth_upgrade_time.value());
      RecordNearbyShareConnectionEstablishedToBandwidthUpgradeDuration(
          medium, upgrade_delta);
      CD_LOG(INFO, Feature::NS) << "Endpoint [" << endpoint_id
                                << "] connection to bandwidth upgrade ("
                                << medium << "): " << upgrade_delta;
    }
  } else {
    // Bandwidth upgrades are requested a number of times, so it is possible for
    // more than one upgrade to complete successfully.
    bool is_first_upgrade =
        !receiver_data_[endpoint_id].bandwidth_upgrade_time.has_value();
    receiver_data_[endpoint_id].upgraded_medium = medium;
    CD_LOG(INFO, Feature::NS)
        << "Endpoint [" << endpoint_id << "] upgraded bandwidth to " << medium;

    // Only log metrics the first time bandwidth upgrade is successful, since
    // subsequent upgrades could pollute these metrics.
    if (is_first_upgrade) {
      // Only store the first time a bandwidth upgrade occurred.
      receiver_data_[endpoint_id].bandwidth_upgrade_time =
          base::TimeTicks::Now();

      // Different upgrade metrics should be logged in high visibility mode.
      if (receiver_data_[endpoint_id].is_high_visibility) {
        // The endpoint must have been decoded at this point.
        if (!receiver_data_[endpoint_id].endpoint_decoded_time.has_value()) {
          return;
        }

        // Compute and log the time delta between when the receiver decodes the
        // incoming endpoint and when the first bandwidth upgrade occurs. This
        // is a parallel to the sender upgrade metric and provides insight into
        // how long a bandwidth upgrade takes to complete from the receiver
        // side.
        base::TimeDelta upgrade_delta = ComputeDelta(
            receiver_data_[endpoint_id].endpoint_decoded_time.value(),
            receiver_data_[endpoint_id].bandwidth_upgrade_time.value());
        RecordNearbyShareHighVisibilityEndpointDecodedToBandwidthUpgradeDuration(
            medium, upgrade_delta);
        CD_LOG(INFO, Feature::NS)
            << "Endpoint [" << endpoint_id << "] decode to bandwidth upgrade ("
            << medium << "): " << upgrade_delta;
      } else {
        // The handshake must have occurred at this point.
        if (!receiver_data_[endpoint_id]
                 .paired_key_handshake_time.has_value()) {
          return;
        }

        // Compute and log the time delta between when the paired key handshake
        // completes and when the first bandwidth upgrade occurs. This provides
        // insight into how long a bandwidth upgrade takes to complete for a
        // non-high visibility mode receiver.
        base::TimeDelta handshake_delta = ComputeDelta(
            receiver_data_[endpoint_id].paired_key_handshake_time.value(),
            receiver_data_[endpoint_id].bandwidth_upgrade_time.value());
        RecordNearbyShareNonHighVisibilityPairedKeyCompleteToBandwidthUpgradeDuration(
            medium, handshake_delta);
        CD_LOG(INFO, Feature::NS)
            << "Endpoint [" << endpoint_id
            << "] handshake complete to bandwidth upgrade (" << medium
            << "): " << handshake_delta;
      }
    }
  }
}