Ash HUD Tracing: Add "Start tracing" button to HUD.

This CL enables simple tracing control from Ash HUD.
Perfetto trace is always saved to /tmp/ash_trace.dat
(hard-coded file name).

DD: https://goto.google.com/ash-tracing

Bug: 1111854
Test: browser_tests --gtest_filter='All/AshHUDLoginTest.AshHUDVerifyTracing/*'

Change-Id: I14a00d2652bb97aa830f194baddfc77164aa4ebc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2597749
Reviewed-by: Dirk Pranke <[email protected]>
Reviewed-by: Mitsuru Oshima <[email protected]>
Reviewed-by: Sami Kyöstilä <[email protected]>
Commit-Queue: Alexander Alekseev <[email protected]>
Cr-Commit-Position: refs/heads/master@{#879106}
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index 12442ea54..d7c50e9 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -515,6 +515,12 @@
     "host/root_window_transformer.h",
     "host/transformer_helper.cc",
     "host/transformer_helper.h",
+    "hud_display/ash_tracing_handler.cc",
+    "hud_display/ash_tracing_handler.h",
+    "hud_display/ash_tracing_manager.cc",
+    "hud_display/ash_tracing_manager.h",
+    "hud_display/ash_tracing_request.cc",
+    "hud_display/ash_tracing_request.h",
     "hud_display/cpu_graph_page_view.cc",
     "hud_display/cpu_graph_page_view.h",
     "hud_display/cpu_status.cc",
@@ -1888,6 +1894,7 @@
     # TODO(msw): Remove this; ash should not depend on blink/webkit.
     "//third_party/blink/public:blink_headers",
     "//third_party/icu",
+    "//third_party/perfetto:libperfetto",
     "//third_party/qcms",
     "//third_party/re2",
     "//ui/accessibility",
diff --git a/ash/hud_display/DEPS b/ash/hud_display/DEPS
new file mode 100644
index 0000000..58d2ae3
--- /dev/null
+++ b/ash/hud_display/DEPS
@@ -0,0 +1,5 @@
+include_rules = [
+  "+services/tracing/public",
+  "+third_party/perfetto/include/perfetto/tracing/core/trace_config.h",
+  "+third_party/perfetto/include/perfetto/tracing/tracing.h",
+]
diff --git a/ash/hud_display/ash_tracing_handler.cc b/ash/hud_display/ash_tracing_handler.cc
new file mode 100644
index 0000000..92f763ee
--- /dev/null
+++ b/ash/hud_display/ash_tracing_handler.cc
@@ -0,0 +1,112 @@
+// Copyright 2021 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 "ash/hud_display/ash_tracing_handler.h"
+
+#include <sys/mman.h>
+
+#include <algorithm>
+
+#include "ash/hud_display/ash_tracing_request.h"
+#include "ash/shell.h"
+#include "base/bind.h"
+#include "base/files/file.h"
+#include "base/files/platform_file.h"
+#include "base/logging.h"
+#include "base/no_destructor.h"
+#include "base/stl_util.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+#include "services/tracing/public/cpp/perfetto/perfetto_config.h"
+#include "third_party/perfetto/include/perfetto/tracing/core/trace_config.h"
+#include "third_party/perfetto/include/perfetto/tracing/tracing.h"
+
+namespace ash {
+namespace hud_display {
+namespace {
+
+std::unique_ptr<perfetto::TracingSession> (*testing_perfetto_session_creator)(
+    void) = nullptr;
+
+}  // anonymous namespace
+
+AshTracingHandler::AshTracingHandler() {
+  // Bind sequence checker.
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+}
+
+AshTracingHandler::~AshTracingHandler() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+}
+
+void AshTracingHandler::Start(AshTracingRequest* request) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK(!request_);
+  DCHECK(!tracing_session_);
+
+  request_ = request;
+  perfetto::TraceConfig perfetto_config = tracing::GetDefaultPerfettoConfig(
+      base::trace_event::TraceConfig(),
+      /*privacy_filtering_enabled=*/false,
+      /*convert_to_legacy_json=*/false,
+      perfetto::protos::gen::ChromeConfig::USER_INITIATED);
+
+  perfetto_config.set_write_into_file(true);
+  tracing_session_ = testing_perfetto_session_creator
+                         ? testing_perfetto_session_creator()
+                         : perfetto::Tracing::NewTrace();
+  tracing_session_->Setup(perfetto_config, request->GetPlatformFile());
+  auto runner = base::SequencedTaskRunnerHandle::Get();
+  base::WeakPtr<AshTracingHandler> weak_ptr = weak_factory_.GetWeakPtr();
+  tracing_session_->SetOnStartCallback([runner, weak_ptr]() {
+    runner->PostTask(
+        FROM_HERE,
+        base::BindOnce(&AshTracingHandler::OnTracingStarted, weak_ptr));
+  });
+  tracing_session_->SetOnStopCallback([runner, weak_ptr]() {
+    runner->PostTask(
+        FROM_HERE,
+        base::BindOnce(&AshTracingHandler::OnTracingFinished, weak_ptr));
+  });
+  tracing_session_->Start();
+}
+
+void AshTracingHandler::Stop() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK(tracing_session_);
+  tracing_session_->Stop();
+}
+
+bool AshTracingHandler::IsStarted() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  return static_cast<bool>(tracing_session_);
+}
+
+// static
+void AshTracingHandler::SetPerfettoTracingSessionCreatorForTesting(
+    std::unique_ptr<perfetto::TracingSession> (*creator)(void)) {
+  DCHECK(!testing_perfetto_session_creator);
+  testing_perfetto_session_creator = creator;
+}
+
+// static
+void AshTracingHandler::ResetPerfettoTracingSessionCreatorForTesting() {
+  DCHECK(testing_perfetto_session_creator);
+  testing_perfetto_session_creator = nullptr;
+}
+
+void AshTracingHandler::OnTracingStarted() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  request_->OnTracingStarted();
+}
+
+void AshTracingHandler::OnTracingFinished() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  tracing_session_.reset();
+  request_->OnTracingFinished();
+}
+
+}  // namespace hud_display
+}  // namespace ash
diff --git a/ash/hud_display/ash_tracing_handler.h b/ash/hud_display/ash_tracing_handler.h
new file mode 100644
index 0000000..edcf086
--- /dev/null
+++ b/ash/hud_display/ash_tracing_handler.h
@@ -0,0 +1,62 @@
+// Copyright 2021 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.
+
+#ifndef ASH_HUD_DISPLAY_ASH_TRACING_HANDLER_H_
+#define ASH_HUD_DISPLAY_ASH_TRACING_HANDLER_H_
+
+#include <memory>
+
+#include "ash/ash_export.h"
+#include "base/files/platform_file.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
+#include "base/trace_event/trace_config.h"
+
+namespace perfetto {
+class TracingSession;
+}
+
+namespace ash {
+namespace hud_display {
+
+class AshTracingRequest;
+
+// Only one instance of this object can exist at a time.
+class ASH_EXPORT AshTracingHandler {
+ public:
+  AshTracingHandler();
+  AshTracingHandler(const AshTracingHandler&) = delete;
+  AshTracingHandler& operator=(const AshTracingHandler&) = delete;
+  ~AshTracingHandler();
+
+  // Initiates tracing start. Observer will be notified with the result.
+  void Start(AshTracingRequest* request);
+
+  // Initiates tracing stop. Observer will be notified with the result.
+  void Stop();
+
+  // Returns true if tracing was started.
+  bool IsStarted() const;
+
+  // This allows to use fake Perfetto sessions for testing.
+  static void SetPerfettoTracingSessionCreatorForTesting(
+      std::unique_ptr<perfetto::TracingSession> (*creator)(void));
+  static void ResetPerfettoTracingSessionCreatorForTesting();
+
+ private:
+  void OnTracingStarted();
+  void OnTracingFinished();
+
+  AshTracingRequest* request_ = nullptr;
+
+  std::unique_ptr<perfetto::TracingSession> tracing_session_;
+
+  SEQUENCE_CHECKER(my_sequence_checker_);
+  base::WeakPtrFactory<AshTracingHandler> weak_factory_{this};
+};
+
+}  // namespace hud_display
+}  // namespace ash
+
+#endif  // ASH_HUD_DISPLAY_ASH_TRACING_HANDLER_H_
diff --git a/ash/hud_display/ash_tracing_manager.cc b/ash/hud_display/ash_tracing_manager.cc
new file mode 100644
index 0000000..9b4bf462
--- /dev/null
+++ b/ash/hud_display/ash_tracing_manager.cc
@@ -0,0 +1,177 @@
+// Copyright 2021 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 "ash/hud_display/ash_tracing_manager.h"
+
+#include <vector>
+
+#include "ash/hud_display/ash_tracing_request.h"
+#include "ash/session/session_controller_impl.h"
+#include "base/compiler_specific.h"
+#include "base/logging.h"
+#include "base/no_destructor.h"
+#include "base/sequence_checker.h"
+#include "base/stl_util.h"
+#include "base/strings/stringprintf.h"
+
+namespace ash {
+namespace hud_display {
+
+AshTracingManager::AshTracingManager() {
+  SessionController::Get()->AddObserver(this);
+}
+
+AshTracingManager::~AshTracingManager() {
+  if (SessionController::Get())
+    SessionController::Get()->RemoveObserver(this);
+}
+
+// static
+AshTracingManager& AshTracingManager::Get() {
+  static base::NoDestructor<AshTracingManager> manager;
+  return *manager;
+}
+
+bool AshTracingManager::IsBusy() const {
+  if (tracing_requests_.empty())
+    return false;
+
+  switch (GetLastRequestStatus()) {
+    case AshTracingRequest::Status::kEmpty:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kInitialized:
+      return true;
+    case AshTracingRequest::Status::kStarted:
+      return false;
+    case AshTracingRequest::Status::kStopping:
+      return true;
+    case AshTracingRequest::Status::kPendingMount:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kWritingFile:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kCompleted:
+      return false;
+  }
+}
+
+bool AshTracingManager::IsTracingStarted() const {
+  if (tracing_requests_.empty())
+    return false;
+
+  switch (GetLastRequestStatus()) {
+    case AshTracingRequest::Status::kEmpty:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kInitialized:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kStarted:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kStopping:
+      return true;
+    case AshTracingRequest::Status::kPendingMount:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kWritingFile:
+      FALLTHROUGH;
+    case AshTracingRequest::Status::kCompleted:
+      return false;
+  }
+}
+
+std::string AshTracingManager::GetStatusMessage() const {
+  std::string result;
+  if (tracing_requests_.empty())
+    return result;
+
+  unsigned started = 0;
+  unsigned waiting_for_login = 0;
+  unsigned writing_trace_file = 0;
+  unsigned completed = 0;
+  for (const auto& request : tracing_requests_) {
+    switch (request->status()) {
+      case AshTracingRequest::Status::kEmpty:
+        FALLTHROUGH;
+      case AshTracingRequest::Status::kInitialized:
+        FALLTHROUGH;
+      case AshTracingRequest::Status::kStarted:
+        FALLTHROUGH;
+      case AshTracingRequest::Status::kStopping:
+        ++started;
+        break;
+      case AshTracingRequest::Status::kPendingMount:
+        ++waiting_for_login;
+        break;
+      case AshTracingRequest::Status::kWritingFile:
+        ++writing_trace_file;
+        break;
+      case AshTracingRequest::Status::kCompleted:
+        ++completed;
+    }
+  }
+  if (started)
+    result = base::StringPrintf("%u active", started);
+
+  if (waiting_for_login) {
+    if (!result.empty())
+      result += ", ";
+
+    result += base::StringPrintf("%u pending login", waiting_for_login);
+  }
+  if (writing_trace_file) {
+    if (!result.empty())
+      result += ", ";
+
+    result += base::StringPrintf("%u writing", writing_trace_file);
+  }
+  if (completed) {
+    if (!result.empty())
+      result += ", ";
+
+    result += base::StringPrintf("%u completed", completed);
+  }
+  if (!result.empty())
+    result = std::string("Tracing: ") + result + ".";
+
+  return result;
+}
+
+void AshTracingManager::Start() {
+  DCHECK(!IsBusy());
+  DCHECK(!IsTracingStarted());
+  tracing_requests_.push_back(std::make_unique<AshTracingRequest>(this));
+}
+
+void AshTracingManager::Stop() {
+  DCHECK(!tracing_requests_.empty());
+  DCHECK_EQ(GetLastRequestStatus(), AshTracingRequest::Status::kStarted);
+  tracing_requests_.back()->Stop();
+}
+
+void AshTracingManager::AddObserver(AshTracingManager::Observer* observer) {
+  observers_.AddObserver(observer);
+}
+
+void AshTracingManager::RemoveObserver(AshTracingManager::Observer* observer) {
+  observers_.RemoveObserver(observer);
+}
+
+void AshTracingManager::OnRequestStatusChanged(AshTracingRequest* request) {
+  for (Observer& observer : observers_)
+    observer.OnTracingStatusChange();
+}
+
+void AshTracingManager::OnFirstSessionStarted() {
+  for (auto& request : tracing_requests_)
+    request->OnUserLoggedIn();
+}
+
+const std::vector<std::unique_ptr<AshTracingRequest>>&
+AshTracingManager::GetTracingRequestsForTesting() const {
+  return tracing_requests_;
+}
+
+AshTracingRequest::Status AshTracingManager::GetLastRequestStatus() const {
+  return tracing_requests_.back()->status();
+}
+
+}  // namespace hud_display
+}  // namespace ash
diff --git a/ash/hud_display/ash_tracing_manager.h b/ash/hud_display/ash_tracing_manager.h
new file mode 100644
index 0000000..050eb9f
--- /dev/null
+++ b/ash/hud_display/ash_tracing_manager.h
@@ -0,0 +1,81 @@
+// Copyright 2021 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.
+
+#ifndef ASH_HUD_DISPLAY_ASH_TRACING_MANAGER_H_
+#define ASH_HUD_DISPLAY_ASH_TRACING_MANAGER_H_
+
+#include <memory>
+
+#include "ash/hud_display/ash_tracing_request.h"
+#include "ash/public/cpp/session/session_observer.h"
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+
+namespace ash {
+namespace hud_display {
+
+// Singleton object to manager Ash tracing sessions.
+class ASH_EXPORT AshTracingManager : public SessionObserver {
+ public:
+  class Observer : public base::CheckedObserver {
+   public:
+    Observer() = default;
+    ~Observer() override = default;
+
+    virtual void OnTracingStatusChange() = 0;
+  };
+
+  AshTracingManager();
+  AshTracingManager(const AshTracingManager&) = delete;
+  AshTracingManager& operator=(const AshTracingManager&) = delete;
+  ~AshTracingManager() override;
+
+  static AshTracingManager& Get();
+
+  // True when tracing is being started or stopped. No control requests are
+  // possible.
+  bool IsBusy() const;
+
+  // True when tracing can be stopped.
+  bool IsTracingStarted() const;
+
+  // Returns user status message.
+  std::string GetStatusMessage() const;
+
+  // Initiates asynchronous tracing start. Observer will be notified with the
+  // result.
+  void Start();
+
+  // Initiates asynchronous tracing stop. Observer will be notified with the
+  // result.
+  void Stop();
+
+  void AddObserver(Observer* observer);
+  void RemoveObserver(Observer* observer);
+
+  // Called by AshTracingRequest to update system state.
+  void OnRequestStatusChanged(AshTracingRequest* request);
+
+  // SessionObserver
+  void OnFirstSessionStarted() override;
+
+  const std::vector<std::unique_ptr<AshTracingRequest>>&
+  GetTracingRequestsForTesting() const;
+
+ private:
+  // Returns status of the last request in tracing_requests_.
+  AshTracingRequest::Status GetLastRequestStatus() const;
+
+  // Only last tracing request can be active. Other can be either finished, or
+  // waiting for user session start to save the trace.
+  std::vector<std::unique_ptr<AshTracingRequest>> tracing_requests_;
+
+  base::ObserverList<Observer> observers_;
+};
+
+}  // namespace hud_display
+}  // namespace ash
+
+#endif  // ASH_HUD_DISPLAY_ASH_TRACING_MANAGER_H_
diff --git a/ash/hud_display/ash_tracing_request.cc b/ash/hud_display/ash_tracing_request.cc
new file mode 100644
index 0000000..f588067
--- /dev/null
+++ b/ash/hud_display/ash_tracing_request.cc
@@ -0,0 +1,409 @@
+// Copyright 2021 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 "ash/hud_display/ash_tracing_request.h"
+
+#include <sys/mman.h>
+#include <sys/sendfile.h>
+
+#include "ash/hud_display/ash_tracing_handler.h"
+#include "ash/hud_display/ash_tracing_manager.h"
+#include "ash/shell.h"
+#include "ash/shell_delegate.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/platform_file.h"
+#include "base/logging.h"
+#include "base/posix/safe_strerror.h"
+#include "base/strings/stringprintf.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
+
+namespace ash {
+namespace hud_display {
+namespace {
+
+// Tests supply their own IO layer generator.
+static std::unique_ptr<AshTraceDestinationIO> (
+    *test_ash_trace_destination_io_creator)(void) = nullptr;
+
+class DefaultAshTraceDestinationIO : public AshTraceDestinationIO {
+ public:
+  ~DefaultAshTraceDestinationIO() override = default;
+
+  // Overrides base::CreateDirectory.
+  bool CreateDirectory(const base::FilePath& path) override {
+    base::File::Error error;
+    if (!base::CreateDirectoryAndGetError(path, &error)) {
+      LOG(ERROR) << "Failed to create Ash trace file directory '"
+                 << path.value() << "' : error " << error;
+      return false;
+    }
+    return true;
+  }
+
+  // Overrides base::File::File(). Returns pair {File file, bool success}.
+  std::tuple<base::File, bool> CreateTracingFile(
+      const base::FilePath& path) override {
+    base::File file(path, base::File::FLAG_CREATE | base::File::FLAG_WRITE);
+    const bool success = file.IsValid();
+    return std::make_tuple(std::move(file), success);
+  }
+
+  // Implements memfd_create(2).
+  std::tuple<base::PlatformFile, bool> CreateMemFD(
+      const char* name,
+      unsigned int flags) override {
+    base::PlatformFile memfd = memfd_create(name, flags);
+    return std::make_tuple(memfd, memfd != base::kInvalidPlatformFile);
+  }
+
+  bool CanWriteFile(base::PlatformFile fd) override {
+    return fd != base::kInvalidPlatformFile;
+  }
+
+  int fstat(base::PlatformFile fd, struct stat* statbuf) override {
+    return ::fstat(fd, statbuf);
+  }
+
+  ssize_t sendfile(base::PlatformFile out_fd,
+                   base::PlatformFile in_fd,
+                   off_t* offset,
+                   size_t size) override {
+    return ::sendfile(out_fd, in_fd, offset, size);
+  }
+};
+
+std::string GenerateTraceFileName(base::Time timestamp) {
+  base::Time::Exploded time_deets;
+  timestamp.LocalExplode(&time_deets);
+  return base::StringPrintf(
+      "ash-trace_%02d%02d%02d-%02d%02d%02d.%03d.dat", time_deets.year,
+      time_deets.month, time_deets.day_of_month, time_deets.hour,
+      time_deets.minute, time_deets.second, time_deets.millisecond);
+}
+
+std::unique_ptr<AshTraceDestination> GenerateTraceDestinationFile(
+    std::unique_ptr<AshTraceDestinationIO> io,
+    const base::FilePath& tracng_directory_path,
+    base::Time timestamp) {
+  if (!io->CreateDirectory(tracng_directory_path))
+    return nullptr;
+
+  base::FilePath path =
+      tracng_directory_path.AppendASCII(GenerateTraceFileName(timestamp));
+  base::File file;
+  bool success;
+  std::tie(file, success) = io->CreateTracingFile(path);
+  if (!success) {
+    LOG(ERROR) << "Failed to create Ash trace '" << path.value() << "' : error "
+               << file.error_details();
+    return nullptr;
+  }
+
+  return std::make_unique<AshTraceDestination>(std::move(io), std::move(path),
+                                               std::move(file),
+                                               base::kInvalidPlatformFile);
+}
+
+std::unique_ptr<AshTraceDestination> GenerateTraceDestinationMemFD(
+    std::unique_ptr<AshTraceDestinationIO> io) {
+  constexpr char kMemFDDebugName[] = "ash-trace-buffer.dat";
+  base::PlatformFile memfd;
+  bool success;
+  std::tie(memfd, success) = io->CreateMemFD(kMemFDDebugName, MFD_CLOEXEC);
+  if (!success) {
+    LOG(ERROR) << "Failed to create memfd for '" << kMemFDDebugName
+               << "', error:" << base::safe_strerror(errno);
+    return nullptr;
+  }
+  return std::make_unique<AshTraceDestination>(std::move(io), base::FilePath(),
+                                               base::File(), memfd);
+}
+
+// Must be called with blocking allowed (i.e. from the thread pool).
+// Returns null pointer in case of error.
+std::unique_ptr<AshTraceDestination> GenerateTraceDestination(
+    std::unique_ptr<AshTraceDestinationIO> io,
+    base::Time timestamp,
+    bool is_logging_redirect_disabled,
+    const base::FilePath& user_downloads_folder) {
+  constexpr char kTracingDir[] = "tracing";
+  constexpr char kGlobalTracingPath[] = "/run/chrome/";
+  if (is_logging_redirect_disabled) {
+    return GenerateTraceDestinationFile(
+        std::move(io),
+        base::FilePath(kGlobalTracingPath).AppendASCII(kTracingDir), timestamp);
+  }
+  if (!user_downloads_folder.empty()) {
+    // User already logged in.
+    return GenerateTraceDestinationFile(
+        std::move(io),
+        base::FilePath(user_downloads_folder.AppendASCII(kTracingDir)),
+        timestamp);
+  }
+  // Need to write trace to the user Downloads folder, but it is not available
+  // yet. Create memfd.
+  return GenerateTraceDestinationMemFD(std::move(io));
+}
+
+base::FilePath GetUserDownloadsFolder() {
+  return Shell::Get()->shell_delegate()->GetPrimaryUserDownloadsFolder();
+}
+
+bool IsLoggingRedirectDisabled() {
+  return Shell::Get()->shell_delegate()->IsLoggingRedirectDisabled();
+}
+
+struct ExportStatus {
+  std::unique_ptr<AshTraceDestination> destination;
+  bool success = false;
+  std::string error_message;
+};
+
+ExportStatus ExportDataOnThreadPool(base::PlatformFile memfd,
+                                    const base::FilePath& user_downloads_folder,
+                                    base::Time timestamp) {
+  ExportStatus result;
+  result.destination = GenerateTraceDestination(
+      test_ash_trace_destination_io_creator
+          ? test_ash_trace_destination_io_creator()
+          : std::make_unique<DefaultAshTraceDestinationIO>(),
+      timestamp,
+      /*is_logging_redirect_disabled=*/false, user_downloads_folder);
+
+  DCHECK(!result.destination->path().empty());
+
+  struct stat statbuf;
+  if (result.destination->io()->fstat(memfd, &statbuf)) {
+    result.error_message = std::string("Failed to stat memfd, error: ") +
+                           base::safe_strerror(errno);
+    LOG(ERROR) << result.error_message;
+    return result;
+  }
+  off_t offset = 0;
+  const ssize_t written = result.destination->io()->sendfile(
+      result.destination->GetPlatformFile(), memfd, &offset, statbuf.st_size);
+  if (written != statbuf.st_size) {
+    const std::string system_error = base::safe_strerror(errno);
+    result.error_message =
+        base::StringPrintf("Stored only %zd trace bytes of %" PRId64 " to '",
+                           written, static_cast<int64_t>(statbuf.st_size)) +
+        result.destination->path().value() + "', error: " + system_error;
+    LOG(ERROR) << result.error_message;
+    return result;
+  }
+  result.success = true;
+  return result;
+}
+
+AshTracingRequest::GenerateTraceDestinationTask
+CreateGenerateTraceDestinationTask(std::unique_ptr<AshTraceDestinationIO> io,
+                                   base::Time timestamp) {
+  return base::BindOnce(&GenerateTraceDestination, std::move(io), timestamp,
+                        IsLoggingRedirectDisabled(), GetUserDownloadsFolder());
+}
+
+}  // anonymous namespace
+
+AshTraceDestinationIO::~AshTraceDestinationIO() = default;
+
+AshTraceDestination::AshTraceDestination() = default;
+
+AshTraceDestination::AshTraceDestination(
+    std::unique_ptr<AshTraceDestinationIO> io,
+    const base::FilePath& path,
+    base::File&& file,
+    base::PlatformFile fd)
+    : io_(std::move(io)), path_(path), file_(std::move(file)), memfd_(fd) {}
+
+AshTraceDestination::~AshTraceDestination() {
+  Done();
+}
+
+base::PlatformFile AshTraceDestination::GetPlatformFile() const {
+  if (memfd_ != base::kInvalidPlatformFile)
+    return memfd_;
+
+  return file_.GetPlatformFile();
+}
+
+bool AshTraceDestination::CanWriteFile() const {
+  return io_->CanWriteFile(GetPlatformFile());
+}
+
+void AshTraceDestination::Done() {
+  if (memfd_ != base::kInvalidPlatformFile) {
+    base::ThreadPool::PostTask(
+        FROM_HERE,
+        {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+         base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
+        base::BindOnce([](base::PlatformFile fd) { close(fd); }, memfd_));
+    memfd_ = base::kInvalidPlatformFile;
+  }
+  if (file_.IsValid()) {
+    base::ThreadPool::PostTask(
+        FROM_HERE,
+        {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+         base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
+        base::BindOnce([](base::File fd) { fd.Close(); }, std::move(file_)));
+  }
+}
+
+AshTracingRequest::AshTracingRequest(AshTracingManager* manager)
+    : timestamp_(base::Time::Now()), tracing_manager_(manager) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  std::unique_ptr<AshTraceDestinationIO> io =
+      test_ash_trace_destination_io_creator
+          ? test_ash_trace_destination_io_creator()
+          : std::make_unique<DefaultAshTraceDestinationIO>();
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE,
+      {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
+      CreateGenerateTraceDestinationTask(std::move(io), timestamp_),
+      base::BindOnce(&AshTracingRequest::OnTraceDestinationInitialized,
+                     weak_factory_.GetWeakPtr()));
+}
+
+AshTracingRequest::~AshTracingRequest() = default;
+
+void AshTracingRequest::Stop() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK(tracing_handler_);
+  status_ = Status::kStopping;
+  tracing_manager_->OnRequestStatusChanged(this);
+  tracing_handler_->Stop();
+}
+
+void AshTracingRequest::OnTracingStarted() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  status_ = Status::kStarted;
+  tracing_manager_->OnRequestStatusChanged(this);
+}
+
+void AshTracingRequest::OnTracingFinished() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  tracing_handler_.reset();
+  if (!trace_destination_->path().empty()) {
+    // Trace was already stored to the real file.
+    status_ = Status::kCompleted;
+    trace_destination_->Done();
+    tracing_manager_->OnRequestStatusChanged(this);
+    return;
+  }
+  status_ = Status::kPendingMount;
+  tracing_manager_->OnRequestStatusChanged(this);
+
+  // User logged in while tracing. Need to start saving file immediately.
+  if (user_logged_in_)
+    StorePendingFile();
+}
+
+// Will trigger trace file write if needed.
+// If trace is already finalized, `on_completed` will be called immediately.
+void AshTracingRequest::OnUserLoggedIn() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  user_logged_in_ = true;
+  StorePendingFile();
+}
+
+base::PlatformFile AshTracingRequest::GetPlatformFile() const {
+  return trace_destination_->GetPlatformFile();
+}
+
+//  static
+void AshTracingRequest::SetAshTraceDestinationIOCreatorForTesting(
+    std::unique_ptr<AshTraceDestinationIO> (*creator)(void)) {
+  CHECK(!test_ash_trace_destination_io_creator);
+  test_ash_trace_destination_io_creator = creator;
+}
+
+//  static
+void AshTracingRequest::ResetAshTraceDestinationIOCreatorForTesting() {
+  test_ash_trace_destination_io_creator = nullptr;
+}
+
+// static
+AshTracingRequest::GenerateTraceDestinationTask
+AshTracingRequest::CreateGenerateTraceDestinationTaskForTesting(
+    std::unique_ptr<AshTraceDestinationIO> io,
+    base::Time timestamp) {
+  return CreateGenerateTraceDestinationTask(std::move(io), timestamp);
+}
+
+const AshTraceDestination* AshTracingRequest::GetTraceDestinationForTesting()
+    const {
+  return trace_destination_.get();
+}
+
+void AshTracingRequest::OnTraceDestinationInitialized(
+    std::unique_ptr<AshTraceDestination> destination) {
+  DCHECK(!trace_destination_.get());
+  trace_destination_ = std::move(destination);
+  status_ = Status::kInitialized;
+  tracing_manager_->OnRequestStatusChanged(this);
+
+  tracing_handler_ = std::make_unique<AshTracingHandler>();
+  tracing_handler_->Start(this);
+}
+
+void AshTracingRequest::OnPendingFileStored(
+    std::unique_ptr<AshTraceDestination> destination,
+    bool success,
+    std::string error_message) {
+  trace_destination_ = std::move(destination);
+  // Cleanup possible errors.
+  trace_destination_->Done();
+  status_ = Status::kCompleted;
+  error_message_ = error_message;
+  tracing_manager_->OnRequestStatusChanged(this);
+}
+
+void AshTracingRequest::StorePendingFile() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  if (status_ != Status::kPendingMount)
+    return;
+
+  if (!user_logged_in_)
+    return;
+
+  base::FilePath user_downloads_folder = GetUserDownloadsFolder();
+  if (user_downloads_folder.empty()) {
+    error_message_ = "No user Downloads folder.";
+    status_ = Status::kCompleted;
+    trace_destination_->Done();
+    tracing_manager_->OnRequestStatusChanged(this);
+    return;
+  }
+
+  status_ = Status::kWritingFile;
+  tracing_manager_->OnRequestStatusChanged(this);
+
+  DCHECK(trace_destination_->CanWriteFile());
+
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE,
+      {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
+      base::BindOnce(&ExportDataOnThreadPool, GetPlatformFile(),
+                     GetUserDownloadsFolder(), timestamp_),
+      base::BindOnce(
+          [](base::WeakPtr<AshTracingRequest> self,
+             ExportStatus export_status) {
+            if (!self)
+              return;
+
+            self->OnPendingFileStored(std::move(export_status.destination),
+                                      export_status.success,
+                                      export_status.error_message);
+          },
+          weak_factory_.GetWeakPtr()));
+}
+
+}  // namespace hud_display
+}  // namespace ash
diff --git a/ash/hud_display/ash_tracing_request.h b/ash/hud_display/ash_tracing_request.h
new file mode 100644
index 0000000..30f0ac5
--- /dev/null
+++ b/ash/hud_display/ash_tracing_request.h
@@ -0,0 +1,186 @@
+// Copyright 2021 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.
+
+#ifndef ASH_HUD_DISPLAY_ASH_TRACING_REQUEST_H_
+#define ASH_HUD_DISPLAY_ASH_TRACING_REQUEST_H_
+
+#include <memory>
+
+#include "base/callback_forward.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/files/platform_file.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
+#include "base/time/time.h"
+
+#include "ash/ash_export.h"
+
+namespace ash {
+namespace hud_display {
+
+class AshTracingManager;
+class AshTracingHandler;
+
+// This is needed for testing to override File IO.
+class ASH_EXPORT AshTraceDestinationIO {
+ public:
+  virtual ~AshTraceDestinationIO();
+
+  // Overrides base::CreateDirectory.
+  virtual bool CreateDirectory(const base::FilePath& path) = 0;
+
+  // Overrides base::File::File(). Returns pair {File file, bool success}.
+  // Test implementation may return success with invalid file.
+  virtual std::tuple<base::File, bool> CreateTracingFile(
+      const base::FilePath& path) = 0;
+
+  // Implements memfd_create(2). Returns pair {int fd, bool success}.
+  // Test implementation may return success with invalid fd.
+  virtual std::tuple<base::PlatformFile, bool> CreateMemFD(
+      const char* name,
+      unsigned int flags) = 0;
+
+  // Takes GetPlatformFile() from AshTraceDestination and returns true if
+  // given fd is valid for storing traces. Checks for -1 in regular case,
+  // and checks internal status in tests.
+  virtual bool CanWriteFile(base::PlatformFile fd) = 0;
+
+  virtual int fstat(base::PlatformFile fd, struct stat* statbuf) = 0;
+
+  virtual ssize_t sendfile(base::PlatformFile out_fd,
+                           base::PlatformFile in_fd,
+                           off_t* offset,
+                           size_t size) = 0;
+};
+
+class ASH_EXPORT AshTraceDestination {
+ public:
+  AshTraceDestination();
+  AshTraceDestination(std::unique_ptr<AshTraceDestinationIO> io,
+                      const base::FilePath& path,
+                      base::File&& file,
+                      base::PlatformFile memfd);
+
+  AshTraceDestination(const AshTraceDestination&) = delete;
+  AshTraceDestination& operator=(const AshTraceDestination&) = delete;
+
+  ~AshTraceDestination();
+
+  const base::FilePath& path() const { return path_; }
+
+  // Returns PlatformFile for storing trace.
+  // Can be memfd or file depending on the setup.
+  base::PlatformFile GetPlatformFile() const;
+
+  // Reurns true if GetPlatformFile() will return valid file descriptor.
+  // In tests when test IO layer is used returns true if test IO layer will
+  // succeed saving file.
+  bool CanWriteFile() const;
+
+  // Close all files.
+  void Done();
+
+  AshTraceDestinationIO* io() const { return io_.get(); }
+
+ private:
+  std::unique_ptr<AshTraceDestinationIO> io_;
+
+  base::FilePath path_;
+  base::File file_;
+  base::PlatformFile memfd_ = base::kInvalidPlatformFile;
+};
+
+class AshTracingRequest {
+ public:
+  enum class Status {
+    kEmpty,         // Object created.
+    kInitialized,   // File data is initialized
+    kStarted,       // Tracing is in progress.
+    kStopping,      // Tracing is being stopped.
+    kPendingMount,  // Tracing is complete, waiting for home directory mount.
+    kWritingFile,   // Writing trace file from memory to file after user login.
+    kCompleted,     // Trace file is written. Object has valid path.
+  };
+
+  // Will start tracing (asynchronously).
+  AshTracingRequest(AshTracingManager* tracing_manager);
+  AshTracingRequest(const AshTracingRequest&) = delete;
+  AshTracingRequest& operator=(const AshTracingRequest&) = delete;
+
+  ~AshTracingRequest();
+
+  void Stop();
+
+  // Receive notifications from AshTracingHandler.
+  void OnTracingStarted();
+  void OnTracingFinished();
+
+  // Will trigger trace file write if needed.
+  void OnUserLoggedIn();
+  // Returns file descriptor that will actually be used for tracing.
+  base::PlatformFile GetPlatformFile() const;
+
+  Status status() const { return status_; }
+  const std::string& error_message() const { return error_message_; }
+
+  // Tests generate specific fake IO.
+  static ASH_EXPORT void SetAshTraceDestinationIOCreatorForTesting(
+      std::unique_ptr<AshTraceDestinationIO> (*creator)(void));
+  static ASH_EXPORT void ResetAshTraceDestinationIOCreatorForTesting();
+
+  // Tests explicitly check AshTraceDestination behavior and they need to
+  // be able to generate ThreadPool tasks to crete AshTraceDestination.
+  // So this function will return a task that can be sent to IO-enabled
+  // sequence runner to create AshTraceDestination.
+  using AshTraceDestinationUniquePtr = std::unique_ptr<AshTraceDestination>;
+  using GenerateTraceDestinationTask =
+      base::OnceCallback<AshTraceDestinationUniquePtr(void)>;
+  ASH_EXPORT static GenerateTraceDestinationTask
+  CreateGenerateTraceDestinationTaskForTesting(
+      std::unique_ptr<AshTraceDestinationIO> io,
+      base::Time timestamp);
+
+  ASH_EXPORT const AshTraceDestination* GetTraceDestinationForTesting() const;
+
+ private:
+  // Starts tracing after `destination` was initialized on the ThreadPool.
+  void OnTraceDestinationInitialized(
+      std::unique_ptr<AshTraceDestination> destination);
+
+  // Marks file export operation completed.
+  void OnPendingFileStored(std::unique_ptr<AshTraceDestination> destination,
+                           bool success,
+                           std::string error_message);
+
+  // Stores memory trace file to permanent location.
+  void StorePendingFile();
+
+  // Trace status
+  Status status_ = Status::kEmpty;
+
+  // When trace was started.
+  const base::Time timestamp_;
+
+  bool user_logged_in_ = false;
+
+  AshTracingManager* tracing_manager_;
+
+  // This object is deleted once tracing is stopped.
+  std::unique_ptr<AshTracingHandler> tracing_handler_;
+
+  // Non-empty if error has occurred.
+  std::string error_message_;
+
+  std::unique_ptr<AshTraceDestination> trace_destination_;
+
+  SEQUENCE_CHECKER(my_sequence_checker_);
+
+  base::WeakPtrFactory<AshTracingRequest> weak_factory_{this};
+};
+
+}  // namespace hud_display
+}  // namespace ash
+
+#endif  // ASH_HUD_DISPLAY_ASH_TRACING_REQUEST_H_
diff --git a/ash/hud_display/graph_page_view_base.cc b/ash/hud_display/graph_page_view_base.cc
index 521b80f..be273af 100644
--- a/ash/hud_display/graph_page_view_base.cc
+++ b/ash/hud_display/graph_page_view_base.cc
@@ -116,6 +116,8 @@
   legend_min_max_button_ = legend_container_->AddChildView(
       std::make_unique<MinMaxButton>(base::BindRepeating(
           &GraphPageViewBase::OnButtonPressed, base::Unretained(this))));
+
+  legend_min_max_button_->SetTooltipText(u"Trigger graph legend");
   SetMinimizeIconToButton(legend_min_max_button_);
 }
 
diff --git a/ash/hud_display/hud_constants.h b/ash/hud_display/hud_constants.h
index 9d4cff1c..b712f70 100644
--- a/ash/hud_display/hud_constants.h
+++ b/ash/hud_display/hud_constants.h
@@ -18,6 +18,8 @@
 
 constexpr SkColor kHUDBackground = SkColorSetARGB(kHUDAlpha, 17, 17, 17);
 constexpr SkColor kHUDLegendBackground = kHUDBackground;
+constexpr SkColor kHUDDisabledButtonColor =
+    SkColorSetA(kHUDDefaultColor, 0xFF * 0.5);
 
 // Radius of rounded corners for tabs.
 // Must be be divisible by 3 to make kTabOverlayWidth integer.
diff --git a/ash/hud_display/hud_display.cc b/ash/hud_display/hud_display.cc
index 41640cb..f7f8b24 100644
--- a/ash/hud_display/hud_display.cc
+++ b/ash/hud_display/hud_display.cc
@@ -39,6 +39,7 @@
 
 // Default HUDDisplayView height.
 constexpr size_t kDefaultHUDGraphHeight = 300;
+constexpr size_t kDefaultHUDSettingHeight = 400;
 
 // Top border + Header height + margin + graph height + bottom border..
 constexpr int kHUDViewDefaultHeight =
@@ -70,6 +71,8 @@
     return hud_display_->NonClientHitTest(point);
   }
 
+  HUDDisplayView* GetHUDDisplayViewForTesting() { return hud_display_; }
+
  private:
   HUDDisplayView* hud_display_;
 };
@@ -136,6 +139,11 @@
   g_hud_widget = widget;
 }
 
+// static
+bool HUDDisplayView::IsShown() {
+  return g_hud_widget;
+}
+
 HUDDisplayView::HUDDisplayView() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);
 
@@ -191,6 +199,14 @@
 
 // There is only one button.
 void HUDDisplayView::OnSettingsToggle() {
+  gfx::Rect bounds = g_hud_widget->GetWindowBoundsInScreen();
+  constexpr int settings_height_addition =
+      kDefaultHUDSettingHeight - kDefaultHUDGraphHeight;
+  // Adjust window height.
+  bounds.set_height(bounds.height() + (settings_view_->GetVisible() ? -1 : 1) *
+                                          settings_height_addition);
+  g_hud_widget->SetBounds(bounds);
+
   settings_view_->ToggleVisibility();
   graphs_container_->SetVisible(!settings_view_->GetVisible());
 }
@@ -206,6 +222,28 @@
       ->SetIsOverlayCandidate(g_hud_overlay_mode);
 }
 
+// static
+HUDDisplayView* HUDDisplayView::GetForTesting() {
+  if (!g_hud_widget)
+    return nullptr;
+
+  HTClientView* client_view =
+      static_cast<HTClientView*>(g_hud_widget->client_view());
+
+  if (!client_view)
+    return nullptr;
+
+  return client_view->GetHUDDisplayViewForTesting();  // IN-TEST
+}
+
+HUDSettingsView* HUDDisplayView::GetSettingsViewForTesting() {
+  return settings_view_;
+}
+
+void HUDDisplayView::ToggleSettingsForTesting() {
+  OnSettingsToggle();
+}
+
 int HUDDisplayView::NonClientHitTest(const gfx::Point& point) {
   const View* view = GetEventHandlerForPoint(point);
   if (!view)
diff --git a/ash/hud_display/hud_display.h b/ash/hud_display/hud_display.h
index 8dc3f3f..e929ea4b 100644
--- a/ash/hud_display/hud_display.h
+++ b/ash/hud_display/hud_display.h
@@ -5,6 +5,7 @@
 #ifndef ASH_HUD_DISPLAY_HUD_DISPLAY_H_
 #define ASH_HUD_DISPLAY_HUD_DISPLAY_H_
 
+#include "ash/ash_export.h"
 #include "base/sequence_checker.h"
 #include "ui/views/view.h"
 
@@ -33,6 +34,9 @@
   // Creates/Destroys global singleton.
   static void Toggle();
 
+  // True when HUD is shown.
+  static bool ASH_EXPORT IsShown();
+
   // Called from ClientView. Responsible for moving widget when clicked outside
   // of the children.
   int NonClientHitTest(const gfx::Point& point);
@@ -49,6 +53,10 @@
   // Changes HUD overlay flag.
   void ToggleOverlay();
 
+  ASH_EXPORT static HUDDisplayView* GetForTesting();
+  ASH_EXPORT HUDSettingsView* GetSettingsViewForTesting();
+  ASH_EXPORT void ToggleSettingsForTesting();
+
  private:
   HUDHeaderView* header_view_ = nullptr;             // not owned
   GraphsContainerView* graphs_container_ = nullptr;  // not owned
diff --git a/ash/hud_display/hud_header_view.cc b/ash/hud_display/hud_header_view.cc
index 6ce1307..27cf43e 100644
--- a/ash/hud_display/hud_header_view.cc
+++ b/ash/hud_display/hud_header_view.cc
@@ -186,9 +186,10 @@
       gfx::Insets(kHUDInset, kHUDInset, 0, kHUDInset)));
 
   // Add buttons and tab strip.
-  header_buttons->AddChildView(
-      std::make_unique<SettingsButton>(base::BindRepeating(
-          &HUDDisplayView::OnSettingsToggle, base::Unretained(hud))));
+  header_buttons
+      ->AddChildView(std::make_unique<SettingsButton>(base::BindRepeating(
+          &HUDDisplayView::OnSettingsToggle, base::Unretained(hud))))
+      ->SetTooltipText(u"Trigger Ash HUD Settings");
   tab_strip_ = header_buttons->AddChildView(std::make_unique<HUDTabStrip>(hud));
 
   // Padding will take the rest of the header and draw bottom inner left
diff --git a/ash/hud_display/hud_settings_view.cc b/ash/hud_display/hud_settings_view.cc
index 1ae2042..672bda1 100644
--- a/ash/hud_display/hud_settings_view.cc
+++ b/ash/hud_display/hud_settings_view.cc
@@ -6,6 +6,7 @@
 
 #include <string>
 
+#include "ash/hud_display/ash_tracing_handler.h"
 #include "ash/hud_display/hud_display.h"
 #include "ash/hud_display/hud_properties.h"
 #include "ash/shell.h"
@@ -22,6 +23,7 @@
 #include "ui/compositor/compositor.h"
 #include "ui/compositor/scoped_animation_duration_scale_mode.h"
 #include "ui/gfx/canvas.h"
+#include "ui/gfx/paint_throbber.h"
 #include "ui/views/background.h"
 #include "ui/views/controls/button/checkbox.h"
 #include "ui/views/controls/button/label_button.h"
@@ -334,19 +336,72 @@
   views::View::Layout();
 }
 
-std::unique_ptr<views::LabelButton> CreateActionButton(
-    views::Button::PressedCallback::Callback callback,
-    const std::u16string& text) {
-  auto button = std::make_unique<views::LabelButton>(callback, text);
-  button->SetHorizontalAlignment(gfx::ALIGN_CENTER);
-  button->SetEnabledTextColors(kHUDBackground);
-  button->SetProperty(kHUDClickHandler, HTCLIENT);
-  constexpr float kActionButtonCournerRadius = 2;
-  button->SetBackground(views::CreateRoundedRectBackground(
-      kHUDDefaultColor, kActionButtonCournerRadius));
-  button->SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
-  return button;
-}
+class HUDActionButton : public views::LabelButton {
+ public:
+  HUDActionButton(views::Button::PressedCallback::Callback callback,
+                  const std::u16string& text)
+      : LabelButton(callback, text) {
+    SetHorizontalAlignment(gfx::ALIGN_CENTER);
+    SetEnabledTextColors(kHUDBackground);
+    SetProperty(kHUDClickHandler, HTCLIENT);
+    constexpr float kActionButtonCournerRadius = 2;
+    SetBackground(views::CreateRoundedRectBackground(
+        kHUDDefaultColor, kActionButtonCournerRadius));
+    SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
+    on_enabled_changed_subscription_ =
+        AddEnabledChangedCallback(base::BindRepeating(
+            &HUDActionButton::OnEnabedChanged, base::Unretained(this)));
+  }
+
+  HUDActionButton(const HUDActionButton&) = delete;
+  HUDActionButton& operator=(const HUDActionButton&) = delete;
+
+  ~HUDActionButton() override = default;
+
+  void PaintButtonContents(gfx::Canvas* canvas) override {
+    views::LabelButton::PaintButtonContents(canvas);
+    if (spinner_refresh_timer_.IsRunning()) {
+      base::Time now = base::Time::Now();
+      gfx::Rect spinner = GetContentsBounds();
+      int spinner_width = std::min(spinner.width(), spinner.height());
+      spinner.ClampToCenteredSize(gfx::Size(spinner_width, spinner_width));
+      gfx::PaintThrobberSpinning(canvas, spinner,
+                                 SkColorSetA(SK_ColorWHITE, 0xFF * (.5)),
+                                 (now - spinner_created_) / 8);
+    }
+  }
+
+  void DisableWithSpinner() {
+    DCHECK(!spinner_refresh_timer_.IsRunning());
+    SetEnabled(false);
+    constexpr base::TimeDelta interval = base::TimeDelta::FromSecondsD(0.5);
+    spinner_created_ = base::Time::Now();
+    spinner_refresh_timer_.Start(
+        FROM_HERE, interval,
+        base::BindRepeating(
+            [](views::View* button) { button->SchedulePaint(); },
+            base::Unretained(this)));
+    SchedulePaint();
+  }
+
+  void UpdateBackgroundColor() override {
+    if (GetVisualState() == STATE_DISABLED) {
+      GetBackground()->SetNativeControlColor(kHUDDisabledButtonColor);
+    } else {
+      GetBackground()->SetNativeControlColor(kHUDDefaultColor);
+    }
+  }
+
+ private:
+  void OnEnabedChanged() {
+    if (GetEnabled())
+      spinner_refresh_timer_.Stop();
+  }
+
+  base::CallbackListSubscription on_enabled_changed_subscription_;
+  base::Time spinner_created_;
+  base::RepeatingTimer spinner_refresh_timer_;
+};
 
 }  // anonymous namespace
 
@@ -444,14 +499,66 @@
           views::BoxLayout::Orientation::kVertical))
       ->set_cross_axis_alignment(
           views::BoxLayout::CrossAxisAlignment::kStretch);
+
+  // Tracing controls.
+  constexpr int kTracingControlButtonMargin = 6;
+  views::View* tracing_controls = AddChildView(std::make_unique<views::View>());
+  tracing_controls
+      ->SetLayoutManager(std::make_unique<views::BoxLayout>(
+          views::BoxLayout::Orientation::kVertical))
+      ->set_cross_axis_alignment(
+          views::BoxLayout::CrossAxisAlignment::kStretch);
+
   ui_devtools_controls->SetBorder(
       views::CreateEmptyBorder(gfx::Insets(kUiDevToolsControlButtonMargin)));
   ui_dev_tools_control_button_ =
-      ui_devtools_controls->AddChildView(CreateActionButton(
+      ui_devtools_controls->AddChildView(std::make_unique<HUDActionButton>(
           base::BindRepeating(&HUDSettingsView::OnEnableUiDevToolsButtonPressed,
                               base::Unretained(this)),
           std::u16string()));
   UpdateDevToolsControlButtonLabel();
+
+  tracing_controls->SetBorder(
+      views::CreateEmptyBorder(gfx::Insets(kTracingControlButtonMargin)));
+  tracing_control_button_ =
+      tracing_controls->AddChildView(std::make_unique<HUDActionButton>(
+          base::BindRepeating(&HUDSettingsView::OnEnableTracingButtonPressed,
+                              base::Unretained(this)),
+          std::u16string()));
+
+  const int kLabelBorderWidth = 3;
+  tracing_status_message_ =
+      tracing_controls->AddChildView(std::make_unique<views::Label>(
+          std::u16string(), views::style::CONTEXT_LABEL));
+  tracing_status_message_->SetAutoColorReadabilityEnabled(false);
+  tracing_status_message_->SetEnabledColor(kHUDDefaultColor);
+  tracing_status_message_->SetBorder(views::CreateEmptyBorder(
+      gfx::Insets(/*vertical=*/0, /*horizontal=*/kLabelBorderWidth)));
+  tracing_status_message_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
+
+  views::Label* pii_label = tracing_controls->AddChildView(std::make_unique<
+                                                           views::Label>(
+      u"WARNING: Trace files may contain Personally Identifiable Information. "
+      u"You should use discretion when sharing your trace files.",
+      views::style::CONTEXT_LABEL));
+  pii_label->SetMultiLine(true);
+  pii_label->SetAutoColorReadabilityEnabled(false);
+  pii_label->SetEnabledColor(kHUDDefaultColor);
+  pii_label->SetBorder(views::CreateEmptyBorder(
+      gfx::Insets(/*vertical=*/0, /*horizontal=*/kLabelBorderWidth)));
+  pii_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
+
+  UpdateTracingControlButton();
+
+  AshTracingManager::Get().AddObserver(this);
+}
+
+HUDSettingsView::~HUDSettingsView() {
+  AshTracingManager::Get().RemoveObserver(this);
+}
+
+void HUDSettingsView::OnTracingStatusChange() {
+  UpdateTracingControlButton();
 }
 
 void HUDSettingsView::OnEnableUiDevToolsButtonPressed(const ui::Event& event) {
@@ -473,8 +580,6 @@
   }
 }
 
-HUDSettingsView::~HUDSettingsView() = default;
-
 void HUDSettingsView::ToggleVisibility() {
   const bool is_shown = !GetVisible();
   if (is_shown) {
@@ -485,6 +590,38 @@
   SetVisible(is_shown);
 }
 
+void HUDSettingsView::OnEnableTracingButtonPressed(const ui::Event& event) {
+  ToggleTracing();
+}
+
+void HUDSettingsView::ToggleTracingForTesting() {
+  ToggleTracing();
+}
+
+void HUDSettingsView::ToggleTracing() {
+  AshTracingManager& manager = AshTracingManager::Get();
+  tracing_control_button_->DisableWithSpinner();
+  if (manager.IsTracingStarted()) {
+    manager.Stop();
+  } else {
+    manager.Start();
+  }
+}
+
+void HUDSettingsView::UpdateTracingControlButton() {
+  AshTracingManager& manager = AshTracingManager::Get();
+  if (!manager.IsBusy())
+    tracing_control_button_->SetEnabled(true);
+
+  tracing_status_message_->SetText(
+      base::ASCIIToUTF16(manager.GetStatusMessage()));
+  if (manager.IsTracingStarted()) {
+    tracing_control_button_->SetText(u"Stop tracing.");
+  } else {
+    tracing_control_button_->SetText(u"Start tracing.");
+  }
+}
+
 }  // namespace hud_display
 
 }  // namespace ash
diff --git a/ash/hud_display/hud_settings_view.h b/ash/hud_display/hud_settings_view.h
index 8a900740..0734234 100644
--- a/ash/hud_display/hud_settings_view.h
+++ b/ash/hud_display/hud_settings_view.h
@@ -8,7 +8,9 @@
 #include <memory>
 #include <vector>
 
+#include "ash/hud_display/ash_tracing_manager.h"
 #include "ash/hud_display/hud_constants.h"
+#include "base/memory/weak_ptr.h"
 #include "ui/views/view.h"
 
 namespace ui {
@@ -16,6 +18,7 @@
 }
 
 namespace views {
+class Label;
 class LabelButton;
 }
 
@@ -25,7 +28,11 @@
 class HUDCheckboxHandler;
 class HUDDisplayView;
 
-class HUDSettingsView : public views::View {
+namespace {
+class HUDActionButton;
+}
+
+class HUDSettingsView : public AshTracingManager::Observer, public views::View {
  public:
   METADATA_HEADER(HUDSettingsView);
 
@@ -35,20 +42,39 @@
   HUDSettingsView(const HUDSettingsView&) = delete;
   HUDSettingsView& operator=(const HUDSettingsView&) = delete;
 
+  // AshTracingManager::Observer
+  void OnTracingStatusChange() override;
+
   // Shows/hides the view.
   void ToggleVisibility();
 
   // Creates Ui Dev Tools.
   void OnEnableUiDevToolsButtonPressed(const ui::Event& event);
 
+  // Starts tracing.
+  void OnEnableTracingButtonPressed(const ui::Event& event);
+
+  ASH_EXPORT void ToggleTracingForTesting();
+
  private:
+  // Starts/Stops tracing.
+  void ToggleTracing();
+
   // Replace "Create Ui Dev Tools" button label with "DevTools running".
   void UpdateDevToolsControlButtonLabel();
 
+  // Switches between "Start tracing" and "Stop tracing" button labels.
+  void UpdateTracingControlButton();
+
   std::vector<std::unique_ptr<HUDCheckboxHandler>> checkbox_handlers_;
 
   // Container for "Create Ui Dev Tools" button or "DevTools running" label.
   views::LabelButton* ui_dev_tools_control_button_ = nullptr;
+
+  HUDActionButton* tracing_control_button_ = nullptr;
+  views::Label* tracing_status_message_ = nullptr;
+
+  base::WeakPtrFactory<HUDSettingsView> weak_factory_{this};
 };
 
 }  // namespace hud_display
diff --git a/ash/public/cpp/test/shell_test_api.h b/ash/public/cpp/test/shell_test_api.h
index 2f8a01d..85e039df 100644
--- a/ash/public/cpp/test/shell_test_api.h
+++ b/ash/public/cpp/test/shell_test_api.h
@@ -21,6 +21,10 @@
 class DisplayManager;
 }
 
+namespace ui {
+class Accelerator;
+}
+
 namespace ash {
 enum class AppListViewState;
 class DragDropController;
@@ -126,6 +130,13 @@
   // shown.
   bool IsContextMenuShown() const;
 
+  // Sends accelerator directly to AcceleratorController.
+  bool IsActionForAcceleratorEnabled(const ui::Accelerator& accelerator) const;
+  bool PressAccelerator(const ui::Accelerator& accelerator);
+
+  // Returns true when Ash HUD is shown.
+  bool IsHUDShown();
+
  private:
   Shell* shell_;  // not owned
 
diff --git a/ash/shell_delegate.h b/ash/shell_delegate.h
index 6d63a36..f923691 100644
--- a/ash/shell_delegate.h
+++ b/ash/shell_delegate.h
@@ -10,6 +10,7 @@
 
 #include "ash/ash_export.h"
 #include "base/callback.h"
+#include "base/files/file_path.h"
 #include "chromeos/services/multidevice_setup/public/mojom/multidevice_setup.mojom-forward.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "services/device/public/mojom/bluetooth_system.mojom-forward.h"
@@ -115,6 +116,13 @@
   virtual void StartUiDevTools() {}
   virtual void StopUiDevTools() {}
   virtual int GetUiDevToolsPort() const;
+
+  // Returns true if Chrome was started with --disable-logging-redirect option.
+  virtual bool IsLoggingRedirectDisabled() const = 0;
+
+  // Returns empty path is user session has not started yet, or path to the
+  // primary user Downloads folder if user has already logged in.
+  virtual base::FilePath GetPrimaryUserDownloadsFolder() const = 0;
 };
 
 }  // namespace ash
diff --git a/ash/shell_test_api.cc b/ash/shell_test_api.cc
index 87a3760a..8a0e1e4 100644
--- a/ash/shell_test_api.cc
+++ b/ash/shell_test_api.cc
@@ -8,9 +8,11 @@
 #include <utility>
 
 #include "ash/accelerators/accelerator_commands.h"
+#include "ash/accelerators/accelerator_controller_impl.h"
 #include "ash/accelerometer/accelerometer_reader.h"
 #include "ash/app_list/app_list_controller_impl.h"
 #include "ash/app_list/views/app_list_view.h"
+#include "ash/hud_display/hud_display.h"
 #include "ash/keyboard/keyboard_controller_impl.h"
 #include "ash/public/cpp/autotest_private_api_utils.h"
 #include "ash/public/cpp/tablet_mode_observer.h"
@@ -263,4 +265,19 @@
   return Shell::GetPrimaryRootWindowController()->IsContextMenuShown();
 }
 
+bool ShellTestApi::IsActionForAcceleratorEnabled(
+    const ui::Accelerator& accelerator) const {
+  return Shell::Get()->accelerator_controller()->IsActionForAcceleratorEnabled(
+      accelerator);
+}
+
+bool ShellTestApi::PressAccelerator(const ui::Accelerator& accelerator) {
+  return Shell::Get()->accelerator_controller()->AcceleratorPressed(
+      accelerator);
+}
+
+bool ShellTestApi::IsHUDShown() {
+  return hud_display::HUDDisplayView::IsShown();
+}
+
 }  // namespace ash
diff --git a/ash/test_shell_delegate.cc b/ash/test_shell_delegate.cc
index a508bd2..074690b 100644
--- a/ash/test_shell_delegate.cc
+++ b/ash/test_shell_delegate.cc
@@ -86,4 +86,12 @@
   session_restore_in_progress_ = in_progress;
 }
 
+bool TestShellDelegate::IsLoggingRedirectDisabled() const {
+  return false;
+}
+
+base::FilePath TestShellDelegate::GetPrimaryUserDownloadsFolder() const {
+  return base::FilePath();
+}
+
 }  // namespace ash
diff --git a/ash/test_shell_delegate.h b/ash/test_shell_delegate.h
index b288e27..638bc57 100644
--- a/ash/test_shell_delegate.h
+++ b/ash/test_shell_delegate.h
@@ -52,6 +52,8 @@
   void SetCanGoBack(bool can_go_back);
   void SetShouldWaitForTouchAck(bool should_wait_for_touch_ack);
   void SetSessionRestoreInProgress(bool in_progress);
+  bool IsLoggingRedirectDisabled() const override;
+  base::FilePath GetPrimaryUserDownloadsFolder() const override;
 
  private:
   // True if the current top window can go back.