chromium/chromeos/ash/components/file_manager/speedometer.cc

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

#include "chromeos/ash/components/file_manager/speedometer.h"

#include <algorithm>
#include <limits>

#include "base/logging.h"
#include "base/time/time.h"

namespace file_manager {

void Speedometer::SetTotalBytes(const int64_t total_bytes) {
  if (total_bytes != total_bytes_) {
    DCHECK_GE(total_bytes, 0);
    // The goalposts are moving. Throw away the current samples.
    samples_.Clear();
    VLOG_IF(1, total_bytes_ > 0)
        << "Total bytes changed from " << total_bytes_ << " to " << total_bytes;
    total_bytes_ = total_bytes;
  }
}

size_t Speedometer::GetSampleCount() const {
  // While the buffer isn't full, we want the CurrentIndex().
  return std::min(samples_.CurrentIndex(), samples_.BufferSize());
}

bool Speedometer::Update(const int64_t bytes) {
  DCHECK_GE(bytes, 0);

  if (total_bytes_ < bytes) {
    VLOG_IF(1, total_bytes_ > 0)
        << "Total bytes changed from " << total_bytes_ << " to " << bytes
        << " to match the already processed bytes";
    total_bytes_ = bytes;
  }

  const base::TimeTicks now = base::TimeTicks::Now();
  if (const auto it = samples_.End()) {
    const Sample& last = **it;
    DCHECK_GE(now, last.time);

    // Drop this sample if we received the previous one less than 3 second ago.
    if (const base::TimeDelta d = now - last.time; d < base::Seconds(3)) {
      VLOG(1) << "Dropped sample {bytes: " << bytes
              << "} as the previous one was received " << d << " ago";
      return false;
    }

    if (bytes < last.bytes) {
      // Progress is going backwards. Throw away the previous samples.
      samples_.Clear();
      VLOG(1) << "Progress went backwards from " << last.bytes << " bytes to "
              << bytes << " bytes";
    }
  }

  samples_.SaveToBuffer({now, bytes});
  return true;
}

base::TimeDelta Speedometer::GetRemainingTime() const {
  auto it = samples_.Begin();
  if (!it) {
    return base::TimeDelta::Max();
  }

  const Sample& first = **it;
  int n = 1;

  double average_bytes = 0;
  double average_time = 0;
  while (++it) {
    const Sample& sample = **it;
    DCHECK_GE(sample.bytes, first.bytes);
    average_bytes += double(sample.bytes - first.bytes);
    DCHECK_GT(sample.time, first.time);
    average_time += (sample.time - first.time).InSecondsF();
    n++;
  }

  DCHECK_EQ(size_t(n), GetSampleCount());
  if (n < 2) {
    return base::TimeDelta::Max();
  }

  average_bytes /= double(n);
  average_time /= double(n);

  DCHECK_LE(average_bytes, total_bytes_ - first.bytes);

  double variance_time = 0;
  double covariance_time_bytes = 0;
  for (auto it2 = samples_.Begin(); it2; ++it2) {
    const Sample& sample = **it2;
    const double time_diff =
        (sample.time - first.time).InSecondsF() - average_time;
    variance_time += time_diff * time_diff;
    covariance_time_bytes +=
        time_diff * (double(sample.bytes - first.bytes) - average_bytes);
  }

  // Speed is the slope of the linear interpolation in bytes per second.
  const double speed = covariance_time_bytes / variance_time;
  DLOG_IF(FATAL, speed < 0)
      << " speed = " << speed << ", variance_time = " << variance_time
      << ", covariance_time_bytes = " << covariance_time_bytes;

  if (!(speed > 0)) {
    return base::TimeDelta::Max();
  }

  // The linear interpolation goes through (average_time, average_bytes).
  const double end_time =
      (double(total_bytes_ - first.bytes) - average_bytes) / speed +
      average_time;
  return base::Seconds(end_time) - (base::TimeTicks::Now() - first.time);
}

}  // namespace file_manager