blob: 45600c7054f02eef17f126118910fff10ae1b3d8 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/download/internal/common/parallel_download_job.h"
#include <algorithm>
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "components/download/internal/common/parallel_download_utils.h"
#include "components/download/public/common/download_create_info.h"
#include "components/download/public/common/download_stats.h"
#include "components/download/public/common/download_url_loader_factory_getter.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_request_context_getter.h"
namespace download {
namespace {
const int kDownloadJobVerboseLevel = 1;
} // namespace
ParallelDownloadJob::ParallelDownloadJob(
DownloadItem* download_item,
std::unique_ptr<DownloadRequestHandleInterface> request_handle,
const DownloadCreateInfo& create_info,
scoped_refptr<download::DownloadURLLoaderFactoryGetter>
url_loader_factory_getter,
net::URLRequestContextGetter* url_request_context_getter,
service_manager::Connector* connector)
: DownloadJobImpl(download_item, std::move(request_handle), true),
initial_request_offset_(create_info.offset),
initial_received_slices_(download_item->GetReceivedSlices()),
content_length_(create_info.total_bytes),
requests_sent_(false),
is_canceled_(false),
range_support_(create_info.accept_range),
url_loader_factory_getter_(std::move(url_loader_factory_getter)),
url_request_context_getter_(url_request_context_getter),
connector_(connector) {}
ParallelDownloadJob::~ParallelDownloadJob() = default;
void ParallelDownloadJob::OnDownloadFileInitialized(
DownloadFile::InitializeCallback callback,
DownloadInterruptReason result,
int64_t bytes_wasted) {
DownloadJobImpl::OnDownloadFileInitialized(std::move(callback), result,
bytes_wasted);
if (result == DOWNLOAD_INTERRUPT_REASON_NONE)
BuildParallelRequestAfterDelay();
}
void ParallelDownloadJob::Cancel(bool user_cancel) {
is_canceled_ = true;
DownloadJobImpl::Cancel(user_cancel);
if (!requests_sent_) {
timer_.Stop();
return;
}
for (auto& worker : workers_)
worker.second->Cancel(user_cancel);
}
void ParallelDownloadJob::Pause() {
DownloadJobImpl::Pause();
if (!requests_sent_) {
timer_.Stop();
return;
}
for (auto& worker : workers_)
worker.second->Pause();
}
void ParallelDownloadJob::Resume(bool resume_request) {
DownloadJobImpl::Resume(resume_request);
if (!resume_request)
return;
// Send parallel requests if the download is paused previously.
if (!requests_sent_) {
if (!timer_.IsRunning())
BuildParallelRequestAfterDelay();
return;
}
for (auto& worker : workers_)
worker.second->Resume();
}
int ParallelDownloadJob::GetParallelRequestCount() const {
return GetParallelRequestCountConfig();
}
int64_t ParallelDownloadJob::GetMinSliceSize() const {
return GetMinSliceSizeConfig();
}
int ParallelDownloadJob::GetMinRemainingTimeInSeconds() const {
return GetParallelRequestRemainingTimeConfig().InSeconds();
}
void ParallelDownloadJob::CancelRequestWithOffset(int64_t offset) {
if (initial_request_offset_ == offset) {
DownloadJobImpl::Cancel(false);
return;
}
auto it = workers_.find(offset);
DCHECK(it != workers_.end());
it->second->Cancel(false);
}
void ParallelDownloadJob::BuildParallelRequestAfterDelay() {
DCHECK(workers_.empty());
DCHECK(!requests_sent_);
DCHECK(!timer_.IsRunning());
timer_.Start(FROM_HERE, GetParallelRequestDelayConfig(), this,
&ParallelDownloadJob::BuildParallelRequests);
}
void ParallelDownloadJob::OnInputStreamReady(
DownloadWorker* worker,
std::unique_ptr<InputStream> input_stream,
std::unique_ptr<DownloadCreateInfo> download_create_info) {
// If server returns a wrong range, abort the parallel request.
bool success = download_create_info->offset == worker->offset();
if (success) {
success = DownloadJob::AddInputStream(std::move(input_stream),
worker->offset(), worker->length());
}
RecordParallelDownloadAddStreamSuccess(
success, range_support_ == RangeRequestSupportType::kSupport);
// Destroy the request if the sink is gone.
if (!success) {
VLOG(kDownloadJobVerboseLevel)
<< "Byte stream arrived after download file is released.";
worker->Cancel(false);
}
}
void ParallelDownloadJob::BuildParallelRequests() {
DCHECK(!requests_sent_);
DCHECK(!is_paused());
if (is_canceled_ ||
download_item_->GetState() != DownloadItem::DownloadState::IN_PROGRESS) {
return;
}
// TODO(qinmin): The size of |slices_to_download| should be no larger than
// |kParallelRequestCount| unless |kParallelRequestCount| is changed after
// a download is interrupted. This could happen if we use finch to config
// the number of parallel requests.
// Get the next |kParallelRequestCount - 1| slices and fork
// new requests. For the remaining slices, they will be handled once some
// of the workers finish their job.
const DownloadItem::ReceivedSlices& received_slices =
download_item_->GetReceivedSlices();
DownloadItem::ReceivedSlices slices_to_download =
FindSlicesToDownload(received_slices);
DCHECK(!slices_to_download.empty());
int64_t first_slice_offset = slices_to_download[0].offset;
// We may build parallel job without slices. The slices can be cleared or
// previous session only has one stream writing to disk. In these cases, fall
// back to non parallel download.
if (initial_request_offset_ > first_slice_offset) {
VLOG(kDownloadJobVerboseLevel)
<< "Received slices data mismatch initial request offset.";
return;
}
// Create more slices for a new download. The initial request may generate
// a received slice.
if (slices_to_download.size() <= 1 && download_item_->GetTotalBytes() > 0) {
int64_t current_bytes_per_second =
std::max(static_cast<int64_t>(1), download_item_->CurrentSpeed());
int64_t remaining_bytes =
download_item_->GetTotalBytes() - download_item_->GetReceivedBytes();
if (remaining_bytes / current_bytes_per_second >
GetMinRemainingTimeInSeconds()) {
// Fork more requests to accelerate, only if one slice is left to download
// and remaining time seems to be long enough.
slices_to_download = FindSlicesForRemainingContent(
first_slice_offset,
content_length_ - first_slice_offset + initial_request_offset_,
GetParallelRequestCount(), GetMinSliceSize());
} else {
RecordParallelDownloadCreationEvent(
ParallelDownloadCreationEvent::FALLBACK_REASON_REMAINING_TIME);
}
}
DCHECK(!slices_to_download.empty());
// If the last received slice is finished, remove the last request which can
// be out of the range of the file. E.g, the file is 100 bytes, and the last
// request's range header will be "Range:100-".
if (!received_slices.empty() && received_slices.back().finished)
slices_to_download.pop_back();
ForkSubRequests(slices_to_download);
RecordParallelDownloadRequestCount(
static_cast<int>(slices_to_download.size()));
requests_sent_ = true;
}
void ParallelDownloadJob::ForkSubRequests(
const DownloadItem::ReceivedSlices& slices_to_download) {
// If the initial request is working on the first hole, don't create parallel
// request for this hole.
bool skip_first_slice = true;
DownloadItem::ReceivedSlices initial_slices_to_download =
FindSlicesToDownload(initial_received_slices_);
if (initial_slices_to_download.size() > 1) {
DCHECK_EQ(initial_request_offset_, initial_slices_to_download[0].offset);
int64_t first_hole_max = initial_slices_to_download[0].offset +
initial_slices_to_download[0].received_bytes;
skip_first_slice = slices_to_download[0].offset <= first_hole_max;
}
for (auto it = slices_to_download.begin(); it != slices_to_download.end();
++it) {
if (skip_first_slice) {
skip_first_slice = false;
continue;
}
DCHECK_GE(it->offset, initial_request_offset_);
// All parallel requests are half open, which sends request headers like
// "Range:50-".
// If server rejects a certain request, others should take over.
CreateRequest(it->offset, DownloadSaveInfo::kLengthFullContent);
}
}
void ParallelDownloadJob::CreateRequest(int64_t offset, int64_t length) {
DCHECK(download_item_);
DCHECK_EQ(DownloadSaveInfo::kLengthFullContent, length);
auto worker = std::make_unique<DownloadWorker>(this, offset, length);
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("parallel_download_job", R"(
semantics {
sender: "Parallel Download"
description:
"Chrome makes parallel request to speed up download of a file."
trigger:
"When user starts a download request, if it would be technically "
"possible, Chrome starts parallel downloading."
data: "None."
destination: WEBSITE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting: "This feature cannot be disabled in settings."
chrome_policy {
DownloadRestrictions {
DownloadRestrictions: 3
}
}
})");
// The parallel requests only use GET method.
std::unique_ptr<DownloadUrlParameters> download_params(
new DownloadUrlParameters(download_item_->GetURL(),
traffic_annotation));
download_params->set_file_path(download_item_->GetFullPath());
download_params->set_last_modified(download_item_->GetLastModifiedTime());
download_params->set_etag(download_item_->GetETag());
download_params->set_offset(offset);
// Setting the length will result in range request to fetch a slice of the
// file.
download_params->set_length(length);
// Subsequent range requests don't need the "If-Range" header.
download_params->set_use_if_range(false);
// Subsequent range requests have the same referrer URL as the original
// download request.
download_params->set_referrer(download_item_->GetReferrerUrl());
download_params->set_referrer_policy(net::URLRequest::NEVER_CLEAR_REFERRER);
// TODO(xingliu): We should not support redirect at all for parallel requests.
// Currently the network service code path still can redirect.
download_params->set_follow_cross_origin_redirects(false);
// Send the request.
worker->SendRequest(std::move(download_params), url_loader_factory_getter_,
url_request_context_getter_, connector_);
DCHECK(workers_.find(offset) == workers_.end());
workers_[offset] = std::move(worker);
}
} // namespace download