| // Copyright 2018 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 <string> |
| |
| #include "base/at_exit.h" |
| #include "base/atomic_sequence_num.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/run_loop.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/values.h" |
| #include "components/cronet/native/test/test_upload_data_provider.h" |
| #include "components/cronet/native/test/test_url_request_callback.h" |
| #include "components/cronet/native/test/test_util.h" |
| #include "cronet_c.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/mock_cert_verifier.h" |
| |
| namespace { |
| |
| // Type of executor to use for a particular benchmark: |
| enum ExecutorType { |
| EXECUTOR_DIRECT, // Direct executor (on network thread). |
| EXECUTOR_THREAD, // Post to main thread. |
| }; |
| |
| // Upload or download benchmark. |
| enum Direction { |
| DIRECTION_UP, |
| DIRECTION_DOWN, |
| }; |
| |
| // Small or large benchmark payload. |
| enum Size { |
| SIZE_LARGE, |
| SIZE_SMALL, |
| }; |
| |
| // Protocol to benchmark. |
| enum Protocol { |
| PROTOCOL_HTTP, |
| PROTOCOL_QUIC, |
| }; |
| |
| // Dictionary of benchmark options. |
| std::unique_ptr<base::DictionaryValue> g_options; |
| |
| // Return a string configuration option. |
| std::string GetConfigString(const char* key) { |
| std::string value; |
| CHECK(g_options->GetString(key, &value)) << "Cannot find key: " << key; |
| return value; |
| } |
| |
| // Return an int configuration option. |
| int GetConfigInt(const char* key) { |
| int value; |
| CHECK(g_options->GetInteger(key, &value)) << "Cannot find key: " << key; |
| return value; |
| } |
| |
| // Put together a benchmark configuration into a benchmark name. |
| // Make it fixed length for more readable tables. |
| // Benchmark names are written to the JSON output file and slurped up by |
| // Telemetry on the host. |
| std::string BuildBenchmarkName(ExecutorType executor, |
| Direction direction, |
| Protocol protocol, |
| int concurrency, |
| int iterations) { |
| std::string name = direction == DIRECTION_UP ? "Up___" : "Down_"; |
| switch (protocol) { |
| case PROTOCOL_HTTP: |
| name += "H_"; |
| break; |
| case PROTOCOL_QUIC: |
| name += "Q_"; |
| break; |
| } |
| name += std::to_string(iterations) + "_" + std::to_string(concurrency) + "_"; |
| switch (executor) { |
| case EXECUTOR_DIRECT: |
| name += "ExDir"; |
| break; |
| case EXECUTOR_THREAD: |
| name += "ExThr"; |
| break; |
| } |
| return name; |
| } |
| |
| // Cronet UploadDataProvider to use for benchmark. |
| class UploadDataProvider : public cronet::test::TestUploadDataProvider { |
| public: |
| // |length| indicates how many bytes to upload. |
| UploadDataProvider(size_t length) |
| : TestUploadDataProvider(cronet::test::TestUploadDataProvider::SYNC, |
| nullptr), |
| length_(length), |
| remaining_(length) {} |
| |
| private: |
| int64_t GetLength() const override { return length_; } |
| |
| // Override of TestUploadDataProvider::Read() to simply report buffers filled. |
| void Read(Cronet_UploadDataSinkPtr upload_data_sink, |
| Cronet_BufferPtr buffer) override { |
| CHECK(remaining_ > 0); |
| size_t buffer_size = Cronet_Buffer_GetSize(buffer); |
| size_t sending = std::min(buffer_size, remaining_); |
| Cronet_UploadDataSink_OnReadSucceeded(upload_data_sink, sending, false); |
| remaining_ -= sending; |
| } |
| |
| const size_t length_; |
| // Count of bytes remaining to be uploaded. |
| size_t remaining_; |
| }; |
| |
| // Cronet UrlRequestCallback to use for benchmarking. |
| class Callback : public cronet::test::TestUrlRequestCallback { |
| public: |
| Callback() |
| : TestUrlRequestCallback(true), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()) {} |
| ~Callback() override { Cronet_UrlRequestCallback_Destroy(callback_); } |
| |
| // Start one repeated UrlRequest. |iterations_completed| is used to keep track |
| // of how many requests have completed. Final iteration should Quit() |
| // |run_loop|. |
| void Start(size_t buffer_size, |
| int iterations, |
| int concurrency, |
| size_t length, |
| const std::string& url, |
| base::AtomicSequenceNumber* iterations_completed, |
| Cronet_EnginePtr engine, |
| ExecutorType executor, |
| Direction direction, |
| base::RunLoop* run_loop) { |
| iterations_ = iterations; |
| concurrency_ = concurrency; |
| length_ = length; |
| url_ = &url; |
| iterations_completed_ = iterations_completed; |
| engine_ = engine; |
| callback_ = CreateUrlRequestCallback(); |
| CHECK(!executor_); |
| switch (executor) { |
| case EXECUTOR_DIRECT: |
| // TestUrlRequestCallback(true) was called above, so parent will create |
| // a direct executor. |
| GetExecutor(); |
| break; |
| case EXECUTOR_THREAD: |
| // Create an executor that posts back to this thread. |
| executor_ = Cronet_Executor_CreateWith(Callback::Execute); |
| Cronet_Executor_SetClientContext(executor_, this); |
| break; |
| } |
| CHECK(executor_); |
| direction_ = direction; |
| buffer_size_ = buffer_size; |
| run_loop_ = run_loop; |
| StartRequest(); |
| } |
| |
| private: |
| // Create and start a UrlRequest. |
| void StartRequest() { |
| Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); |
| Cronet_UrlRequestParamsPtr request_params = |
| Cronet_UrlRequestParams_Create(); |
| if (direction_ == DIRECTION_UP) { |
| // Create and set an UploadDataProvider on the UrlRequest. |
| upload_data_provider_ = std::make_unique<UploadDataProvider>(length_); |
| cronet_upload_data_provider_ = |
| upload_data_provider_->CreateUploadDataProvider(); |
| Cronet_UrlRequestParams_upload_data_provider_set( |
| request_params, cronet_upload_data_provider_); |
| // Set Content-Type header. |
| Cronet_HttpHeaderPtr header = Cronet_HttpHeader_Create(); |
| Cronet_HttpHeader_name_set(header, "Content-Type"); |
| Cronet_HttpHeader_value_set(header, "application/octet-stream"); |
| Cronet_UrlRequestParams_request_headers_add(request_params, header); |
| Cronet_HttpHeader_Destroy(header); |
| } |
| Cronet_UrlRequest_InitWithParams(request, engine_, url_->c_str(), |
| request_params, callback_, executor_); |
| Cronet_UrlRequestParams_Destroy(request_params); |
| Cronet_UrlRequest_Start(request); |
| } |
| |
| void OnResponseStarted(Cronet_UrlRequestPtr request, |
| Cronet_UrlResponseInfoPtr info) override { |
| CHECK_EQ(200, Cronet_UrlResponseInfo_http_status_code_get(info)); |
| response_step_ = ON_RESPONSE_STARTED; |
| Cronet_BufferPtr buffer = Cronet_Buffer_Create(); |
| Cronet_Buffer_InitWithAlloc(buffer, buffer_size_); |
| StartNextRead(request, buffer); |
| } |
| |
| void OnSucceeded(Cronet_UrlRequestPtr request, |
| Cronet_UrlResponseInfoPtr info) override { |
| Cronet_UrlRequest_Destroy(request); |
| if (cronet_upload_data_provider_) |
| Cronet_UploadDataProvider_Destroy(cronet_upload_data_provider_); |
| |
| int iteration = iterations_completed_->GetNext(); |
| // If this was the final iteration, quit the RunLoop. |
| if (iteration == (iterations_ - 1)) |
| run_loop_->Quit(); |
| // Don't start another request if complete. |
| if (iteration >= (iterations_ - concurrency_)) |
| return; |
| // Start another request. |
| StartRequest(); |
| } |
| |
| void OnFailed(Cronet_UrlRequestPtr request, |
| Cronet_UrlResponseInfoPtr info, |
| Cronet_ErrorPtr error) override { |
| CHECK(false) << "Request failed with error " |
| << Cronet_Error_error_code_get(error); |
| } |
| |
| // A simple executor that posts back to |task_runner_|. |
| static void Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr runnable) { |
| auto* callback = |
| static_cast<Callback*>(Cronet_Executor_GetClientContext(self)); |
| callback->task_runner_->PostTask( |
| FROM_HERE, cronet::test::RunnableWrapper::CreateOnceClosure(runnable)); |
| } |
| |
| Direction direction_; |
| int iterations_; |
| int concurrency_; |
| size_t length_; |
| const std::string* url_; |
| base::AtomicSequenceNumber* iterations_completed_; |
| Cronet_EnginePtr engine_; |
| Cronet_UrlRequestCallbackPtr callback_; |
| Cronet_UploadDataProviderPtr cronet_upload_data_provider_ = nullptr; |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner_; |
| base::RunLoop* run_loop_; |
| size_t buffer_size_; |
| std::unique_ptr<UploadDataProvider> upload_data_provider_; |
| }; |
| |
| // An individual benchmark instance. |
| class Benchmark { |
| public: |
| ~Benchmark() { Cronet_Engine_Destroy(engine_); } |
| |
| // Run and time the benchmark. |
| static void Run(ExecutorType executor, |
| Direction direction, |
| Size size, |
| Protocol protocol, |
| int concurrency, |
| base::DictionaryValue* results) { |
| std::string resource; |
| int iterations; |
| size_t length; |
| switch (size) { |
| case SIZE_SMALL: |
| resource = GetConfigString("SMALL_RESOURCE"); |
| iterations = GetConfigInt("SMALL_ITERATIONS"); |
| length = GetConfigInt("SMALL_RESOURCE_SIZE"); |
| break; |
| case SIZE_LARGE: |
| // When measuring a large upload, only download a small amount so |
| // download time isn't significant. |
| resource = GetConfigString( |
| direction == DIRECTION_UP ? "SMALL_RESOURCE" : "LARGE_RESOURCE"); |
| iterations = GetConfigInt("LARGE_ITERATIONS"); |
| length = GetConfigInt("LARGE_RESOURCE_SIZE"); |
| break; |
| } |
| std::string name = BuildBenchmarkName(executor, direction, protocol, |
| concurrency, iterations); |
| std::string scheme; |
| std::string host; |
| int port; |
| switch (protocol) { |
| case PROTOCOL_HTTP: |
| scheme = "http"; |
| host = GetConfigString("HOST_IP"); |
| port = GetConfigInt("HTTP_PORT"); |
| break; |
| case PROTOCOL_QUIC: |
| scheme = "https"; |
| host = GetConfigString("HOST"); |
| port = GetConfigInt("QUIC_PORT"); |
| break; |
| } |
| std::string url = |
| scheme + "://" + host + ":" + std::to_string(port) + "/" + resource; |
| size_t buffer_size = length > (size_t)GetConfigInt("MAX_BUFFER_SIZE") |
| ? GetConfigInt("MAX_BUFFER_SIZE") |
| : length; |
| Benchmark(executor, direction, size, protocol, concurrency, iterations, |
| length, buffer_size, name, url, host, port, results) |
| .RunInternal(); |
| } |
| |
| private: |
| Benchmark(ExecutorType executor, |
| Direction direction, |
| Size size, |
| Protocol protocol, |
| int concurrency, |
| int iterations, |
| size_t length, |
| size_t buffer_size, |
| const std::string& name, |
| const std::string& url, |
| const std::string& host, |
| int port, |
| base::DictionaryValue* results) |
| : iterations_(iterations), |
| concurrency_(concurrency), |
| length_(length), |
| buffer_size_(buffer_size), |
| name_(name), |
| url_(url), |
| callbacks_(concurrency), |
| executor_(executor), |
| direction_(direction), |
| results_(results) { |
| Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); |
| // Add Host Resolver Rules. |
| std::string host_resolver_rules = |
| "MAP test.example.com " + GetConfigString("HOST_IP") + ","; |
| Cronet_EngineParams_experimental_options_set( |
| engine_params, |
| base::StringPrintf( |
| "{ \"HostResolverRules\": { \"host_resolver_rules\" : \"%s\" } }", |
| host_resolver_rules.c_str()) |
| .c_str()); |
| // Create Cronet Engine. |
| engine_ = Cronet_Engine_Create(); |
| if (protocol == PROTOCOL_QUIC) { |
| Cronet_EngineParams_enable_quic_set(engine_params, true); |
| // Set QUIC hint. |
| Cronet_QuicHintPtr quic_hint = Cronet_QuicHint_Create(); |
| Cronet_QuicHint_host_set(quic_hint, host.c_str()); |
| Cronet_QuicHint_port_set(quic_hint, port); |
| Cronet_QuicHint_alternate_port_set(quic_hint, port); |
| Cronet_EngineParams_quic_hints_add(engine_params, quic_hint); |
| Cronet_QuicHint_Destroy(quic_hint); |
| // Set Mock Cert Verifier. |
| auto cert_verifier = std::make_unique<net::MockCertVerifier>(); |
| cert_verifier->set_default_result(net::OK); |
| Cronet_Engine_SetMockCertVerifierForTesting(engine_, |
| cert_verifier.release()); |
| } |
| |
| // Start Cronet Engine. |
| Cronet_Engine_StartWithParams(engine_, engine_params); |
| Cronet_EngineParams_Destroy(engine_params); |
| } |
| |
| // Run and time the benchmark. |
| void RunInternal() { |
| base::RunLoop run_loop; |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| // Start all concurrent requests. |
| for (auto& callback : callbacks_) { |
| callback.Start(buffer_size_, iterations_, concurrency_, length_, url_, |
| &iterations_completed_, engine_, executor_, direction_, |
| &run_loop); |
| } |
| run_loop.Run(); |
| base::TimeDelta run_time = base::TimeTicks::Now() - start_time; |
| results_->SetInteger(name_, static_cast<int>(run_time.InMilliseconds())); |
| } |
| |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner_; |
| const int iterations_; |
| const int concurrency_; |
| const size_t length_; |
| const size_t buffer_size_; |
| const std::string name_; |
| const std::string url_; |
| std::vector<Callback> callbacks_; |
| base::AtomicSequenceNumber iterations_completed_; |
| Cronet_EnginePtr engine_; |
| const ExecutorType executor_; |
| const Direction direction_; |
| base::DictionaryValue* const results_; |
| }; |
| |
| } // namespace |
| |
| void PerfTest(const char* json_args) { |
| base::AtExitManager exit_manager; |
| |
| // Parse benchmark options into |g_options|. |
| std::string benchmark_options = json_args; |
| std::unique_ptr<base::Value> options_value = |
| base::JSONReader::Read(benchmark_options); |
| CHECK(options_value) << "Parsing benchmark options failed: " |
| << benchmark_options; |
| g_options = base::DictionaryValue::From(std::move(options_value)); |
| CHECK(g_options) << "Benchmark options string is not a dictionary: " |
| << benchmark_options |
| << " See DEFAULT_BENCHMARK_CONFIG in perf_test_util.py."; |
| |
| // Run benchmarks putting timing results into |results|. |
| base::DictionaryValue results; |
| for (ExecutorType executor : {EXECUTOR_DIRECT, EXECUTOR_THREAD}) { |
| for (Direction direction : {DIRECTION_DOWN, DIRECTION_UP}) { |
| for (Protocol protocol : {PROTOCOL_HTTP, PROTOCOL_QUIC}) { |
| // Run large and small benchmarks one at a time to test single-threaded |
| // use. Also run them four at a time to see how they benefit from |
| // concurrency. The value four was chosen as many devices are now |
| // quad-core. |
| Benchmark::Run(executor, direction, SIZE_LARGE, protocol, 1, &results); |
| Benchmark::Run(executor, direction, SIZE_LARGE, protocol, 4, &results); |
| Benchmark::Run(executor, direction, SIZE_SMALL, protocol, 1, &results); |
| Benchmark::Run(executor, direction, SIZE_SMALL, protocol, 4, &results); |
| // Large benchmarks are generally bandwidth bound and unaffected by |
| // per-request overhead. Small benchmarks are not, so test at |
| // further increased concurrency to see if further benefit is possible. |
| Benchmark::Run(executor, direction, SIZE_SMALL, protocol, 8, &results); |
| } |
| } |
| } |
| |
| // Write |results| into results file. |
| std::string results_string; |
| base::JSONWriter::Write(results, &results_string); |
| FILE* results_file = fopen(GetConfigString("RESULTS_FILE").c_str(), "wb"); |
| fwrite(results_string.c_str(), results_string.length(), 1, results_file); |
| fclose(results_file); |
| fclose(fopen(GetConfigString("DONE_FILE").c_str(), "wb")); |
| } |