chromium/chrome/browser/metrics/perf/perf_output_unittest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/metrics/perf/perf_output.h"

#include <stdio.h>
#include <unistd.h>

#include <utility>

#include "base/files/file.h"
#include "base/posix/eintr_wrapper.h"
#include "base/task/sequenced_task_runner.h"
#include "base/threading/thread_restrictions.h"
#include "chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/sampled_profile.pb.h"

namespace metrics {

namespace {
// Returns an example PerfDataProto. The contents don't have to make sense. They
// just need to constitute a semantically valid protobuf.
// |proto| is an output parameter that will contain the created protobuf.
PerfDataProto GetExamplePerfDataProto() {
  PerfDataProto proto;
  proto.set_timestamp_sec(1435604013);  // Time since epoch in seconds.

  PerfDataProto_PerfFileAttr* file_attr = proto.add_file_attrs();
  file_attr->add_ids(61);
  file_attr->add_ids(62);
  file_attr->add_ids(63);

  PerfDataProto_PerfEventAttr* attr = file_attr->mutable_attr();
  attr->set_type(1);
  attr->set_size(2);
  attr->set_config(3);
  attr->set_sample_period(4);
  attr->set_sample_freq(5);

  PerfDataProto_PerfEventStats* stats = proto.mutable_stats();
  stats->set_num_events_read(100);
  stats->set_num_sample_events(200);
  stats->set_num_mmap_events(300);
  stats->set_num_fork_events(400);
  stats->set_num_exit_events(500);

  return proto;
}

// Perf session ID returned by the GetPerfOutputV2 DBus method call.
const uint64_t kFakePerfSssionId = 101;
// Quipper command line arguments for running perf.
const std::vector<std::string> kQuipperArgs{
    "--duration", "4",      "--", "perf", "record", "-a",
    "-e",         "cycles", "-g", "-c",   "4000037"};

// This fakes DebugDaemonClient by serving example perf data when the profiling
// duration elapses.
class FakeDebugDaemonClient : public ash::FakeDebugDaemonClient {
 public:
  FakeDebugDaemonClient()
      : task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}

  FakeDebugDaemonClient(const FakeDebugDaemonClient&) = delete;
  FakeDebugDaemonClient& operator=(const FakeDebugDaemonClient&) = delete;

  ~FakeDebugDaemonClient() override {
    EXPECT_FALSE(perf_output_file_.IsValid());
  }

  void GetPerfOutput(const std::vector<std::string>& quipper_args,
                     bool disable_cpu_idle,
                     int file_descriptor,
                     chromeos::DBusMethodCallback<uint64_t> callback) override {
    // We will write perf output to this pipe FD. dup() |file_descriptor|
    // because it is closed after this method returns.
    base::ScopedPlatformFile perf_output_fd(HANDLE_EINTR(dup(file_descriptor)));
    ASSERT_NE(perf_output_fd, -1);

    perf_output_file_ = base::File(std::move(perf_output_fd));
    ASSERT_TRUE(perf_output_file_.IsValid());

    // Returns a fake perf session ID after calling GetPerfOutputFd.
    task_runner_->PostTask(
        FROM_HERE, base::BindOnce(
                       [](chromeos::DBusMethodCallback<uint64_t> callback) {
                         std::move(callback).Run(kFakePerfSssionId);
                       },
                       std::move(callback)));
  }

  bool stop_called() const { return stop_called_; }

  void StopPerf(uint64_t session_id,
                chromeos::VoidDBusMethodCallback callback) override {
    // Simulates stopping the perf session by writing perf data right away.
    DCHECK(task_runner_->RunsTasksInCurrentSequence());
    EXPECT_EQ(session_id, kFakePerfSssionId);
    stop_called_ = true;
    OnFakePerfOutputComplete();
  }

  // This simulates that profile collection is done and quipper writes perf data
  // over the pipe.
  void OnFakePerfOutputComplete() {
    base::ScopedAllowBlockingForTesting allow_block;

    auto perf_data = GetExamplePerfDataProto().SerializeAsString();
    auto bytes_written = perf_output_file_.WriteAtCurrentPos(perf_data.c_str(),
                                                             perf_data.size());
    EXPECT_EQ(bytes_written, static_cast<ssize_t>(perf_data.size()));
    // Need to close the pipe to unblock the pipe reader.
    perf_output_file_.Close();
  }

 private:
  scoped_refptr<base::SequencedTaskRunner> task_runner_;
  base::File perf_output_file_;
  chromeos::DBusMethodCallback<uint64_t> get_perf_outjput_callback_;
  bool stop_called_ = false;
};

}  // namespace

class PerfOutputCallTest : public testing::Test {
 public:
  PerfOutputCallTest() = default;

  PerfOutputCallTest(const PerfOutputCallTest&) = delete;
  PerfOutputCallTest& operator=(const PerfOutputCallTest&) = delete;

  void SetUp() override {
    debug_daemon_client_ = std::make_unique<FakeDebugDaemonClient>();
  }

  void TearDown() override { perf_output_call_.reset(); }

  void OnPerfOutputComplete(std::string perf_output) {
    perf_output_ = std::move(perf_output);
  }

 protected:
  // |task_environment_| must be the first member (or at least before
  // any member that cares about tasks) to be initialized first and
  // destroyed last.
  content::BrowserTaskEnvironment task_environment_;

  std::unique_ptr<FakeDebugDaemonClient> debug_daemon_client_;
  std::unique_ptr<PerfOutputCall> perf_output_call_;

  std::string perf_output_;
};

// Test getting perf output after profile duration elapses.
TEST_F(PerfOutputCallTest, GetPerfOutput) {
  perf_output_call_ = std::make_unique<PerfOutputCall>(
      debug_daemon_client_.get(), kQuipperArgs, false,
      base::BindOnce(&PerfOutputCallTest::OnPerfOutputComplete,
                     base::Unretained(this)));
  // Not yet collected.
  EXPECT_EQ(perf_output_, "");

  // Perf data is available.
  debug_daemon_client_->OnFakePerfOutputComplete();

  // Note that we can call RunUntilIdle() only after fake perf data is written
  // over the pipe, or RunUntilIdle() will block forever on the reading end.
  task_environment_.RunUntilIdle();

  EXPECT_FALSE(debug_daemon_client_->stop_called());
  EXPECT_EQ(perf_output_, GetExamplePerfDataProto().SerializeAsString());
}

// Test stopping the perf session and get perf output right away.
TEST_F(PerfOutputCallTest, Stop) {
  perf_output_call_ = std::make_unique<PerfOutputCall>(
      debug_daemon_client_.get(), kQuipperArgs, false,
      base::BindOnce(&PerfOutputCallTest::OnPerfOutputComplete,
                     base::Unretained(this)));
  // Not yet collected.
  EXPECT_EQ(perf_output_, "");

  // Perf data is available after calling Stop().
  perf_output_call_->Stop();

  // Note that we can call RunUntilIdle() only after fake perf data is written
  // over the pipe, or RunUntilIdle() will block forever on the reading end.
  task_environment_.RunUntilIdle();

  EXPECT_TRUE(debug_daemon_client_->stop_called());
  EXPECT_EQ(perf_output_, GetExamplePerfDataProto().SerializeAsString());
}

}  // namespace metrics