blob: d39cf384267b5b0fc47b3f31520edb1ae8bff768 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/memory_internals_ui.h"
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/allocator/partition_allocator/src/partition_alloc/partition_alloc_buildflags.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/process/process_handle.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiling_host/profiling_process_host.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/memory_internals_resources.h"
#include "chrome/grit/memory_internals_resources_map.h"
#include "components/heap_profiling/multi_process/supervisor.h"
#include "components/services/heap_profiling/public/cpp/settings.h"
#include "components/services/heap_profiling/public/mojom/heap_profiling_service.mojom.h"
#include "content/public/browser/browser_child_process_host_iterator.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/browser/web_ui_message_handler.h"
#include "content/public/common/process_type.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "ui/shell_dialogs/select_file_dialog.h"
#include "ui/shell_dialogs/select_file_policy.h"
#include "ui/shell_dialogs/selected_file_info.h"
using heap_profiling::Mode;
using heap_profiling::ProfilingProcessHost;
namespace {
// Returns the string to display at the top of the page for help.
std::string GetMessageString() {
#if BUILDFLAG(USE_ALLOCATOR_SHIM)
Mode mode = Mode::kNone;
if (heap_profiling::Supervisor::GetInstance()->HasStarted()) {
mode = heap_profiling::Supervisor::GetInstance()->GetMode();
}
switch (mode) {
case Mode::kAll:
return std::string("Memory logging is enabled for all processes.");
case Mode::kAllRenderers:
return std::string("Memory logging is enabled for all renderers.");
case Mode::kBrowser:
return std::string(
"Memory logging is enabled for just the browser process.");
case Mode::kGpu:
return std::string("Memory logging is enabled for just the gpu process.");
case Mode::kMinimal:
return std::string(
"Memory logging is enabled for the browser and GPU processes.");
case Mode::kRendererSampling:
return std::string(
"Memory logging is enabled for at most one renderer process. Each "
"renderer process has a fixed probability of being sampled at "
"startup.");
case Mode::kUtilitySampling:
return std::string(
"Each utility process has a fixed probability of being profiled at "
"startup.");
case Mode::kNone:
case Mode::kManual:
default:
return std::string(
"Memory logging must be manually enabled for each process via "
"chrome://memory-internals.");
}
#elif defined(ADDRESS_SANITIZER)
return "Memory logging is not available in this build because a memory "
"sanitizer is running.";
#else
return "Memory logging is not available in this build because "
"USE_ALLOCATOR_SHIM is not set. It can not have sanitizers enabled "
"and on Windows it must be a release non-component build.";
#endif
}
// Generates one row of the returned process info.
base::Value::List MakeProcessInfo(int pid, std::string description) {
base::Value::List result;
result.Append(pid);
result.Append(std::move(description));
return result;
}
// Some child processes have good descriptions and some don't, this function
// returns the best it can given the data.
std::string GetChildDescription(const content::ChildProcessData& data) {
if (!data.name.empty()) {
return base::UTF16ToUTF8(data.name);
}
return content::GetProcessTypeNameInEnglish(data.process_type);
}
void CreateAndAddMemoryInternalsUIHTMLSource(Profile* profile) {
content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd(
profile, chrome::kChromeUIMemoryInternalsHost);
source->AddResourcePaths(base::make_span(kMemoryInternalsResources,
kMemoryInternalsResourcesSize));
source->SetDefaultResource(IDR_MEMORY_INTERNALS_MEMORY_INTERNALS_HTML);
}
class MemoryInternalsDOMHandler : public content::WebUIMessageHandler,
ui::SelectFileDialog::Listener {
public:
explicit MemoryInternalsDOMHandler(content::WebUI* web_ui);
MemoryInternalsDOMHandler(const MemoryInternalsDOMHandler&) = delete;
MemoryInternalsDOMHandler& operator=(const MemoryInternalsDOMHandler&) =
delete;
~MemoryInternalsDOMHandler() override;
// WebUIMessageHandler implementation.
void RegisterMessages() override;
// Callback for the "requestProcessList" message.
void HandleRequestProcessList(const base::Value::List& args);
// Callback for the "saveDump" message.
void HandleSaveDump(const base::Value::List& args);
// Callback for the "startProfiling" message.
void HandleStartProfiling(const base::Value::List& args);
protected:
// WebUIMessageHandler implementation.
void OnJavascriptDisallowed() override;
private:
// Sends a request for a process list, and posts the result to
// ReturnProcessListOnUIThread(). Takes ownership of `callback_id` so it can
// be bound to the posted task without copying.
void RequestProcessList(base::Value callback_id, bool success);
void ReturnProcessListOnUIThread(const base::Value& callback_id,
std::vector<base::Value::List> children,
std::vector<base::ProcessId> profiled_pids);
// SelectFileDialog::Listener implementation:
void FileSelected(const ui::SelectedFileInfo& file,
int index,
void* params) override;
void FileSelectionCanceled(void* params) override;
void SaveTraceFinished(bool success);
scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
#if !BUILDFLAG(IS_ANDROID)
raw_ptr<content::WebUI> web_ui_; // The WebUI that owns us.
#endif
base::WeakPtrFactory<MemoryInternalsDOMHandler> weak_factory_{this};
};
MemoryInternalsDOMHandler::MemoryInternalsDOMHandler(content::WebUI* web_ui)
#if !BUILDFLAG(IS_ANDROID)
: web_ui_(web_ui)
#endif
{
}
MemoryInternalsDOMHandler::~MemoryInternalsDOMHandler() {
if (select_file_dialog_) {
select_file_dialog_->ListenerDestroyed();
}
}
void MemoryInternalsDOMHandler::RegisterMessages() {
// Unretained should be OK here since this class is bound to the lifetime of
// the WebUI.
web_ui()->RegisterMessageCallback(
"requestProcessList",
base::BindRepeating(&MemoryInternalsDOMHandler::HandleRequestProcessList,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"saveDump",
base::BindRepeating(&MemoryInternalsDOMHandler::HandleSaveDump,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"startProfiling",
base::BindRepeating(&MemoryInternalsDOMHandler::HandleStartProfiling,
base::Unretained(this)));
}
void MemoryInternalsDOMHandler::HandleRequestProcessList(
const base::Value::List& args) {
AllowJavascript();
CHECK_EQ(args.size(), 1u);
RequestProcessList(args[0].Clone(), /*success=*/true);
}
void MemoryInternalsDOMHandler::HandleSaveDump(const base::Value::List&) {
base::FilePath default_file = base::FilePath().AppendASCII(
base::StringPrintf("trace_with_heap_dump.json.gz"));
AllowJavascript();
#if BUILDFLAG(IS_ANDROID)
base::Value result("Saving...");
FireWebUIListener("save-dump-progress", result);
// On Android write to the user data dir.
// TODO(bug 757115) Does it make sense to show the Android file picker here
// instead? Need to test what that looks like.
base::FilePath user_data_dir;
base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
base::FilePath output_path = user_data_dir.Append(default_file);
ProfilingProcessHost::GetInstance()->SaveTraceWithHeapDumpToFile(
std::move(output_path),
base::BindOnce(&MemoryInternalsDOMHandler::SaveTraceFinished,
weak_factory_.GetWeakPtr()),
false);
#else
if (select_file_dialog_) {
return; // Currently running, wait for existing save to complete.
}
select_file_dialog_ = ui::SelectFileDialog::Create(
this,
std::make_unique<ChromeSelectFilePolicy>(web_ui_->GetWebContents()));
select_file_dialog_->SelectFile(
ui::SelectFileDialog::SELECT_SAVEAS_FILE, std::u16string(), default_file,
nullptr, 0, FILE_PATH_LITERAL(".json.gz"),
web_ui_->GetWebContents()->GetTopLevelNativeWindow(), nullptr);
#endif
}
void MemoryInternalsDOMHandler::HandleStartProfiling(
const base::Value::List& args) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
CHECK_EQ(args.size(), 2u);
const base::Value& callback_id = args[0];
const base::ProcessId pid = args[1].GetInt();
// Refresh to get the updated state of the profiled process after profiling
// starts.
heap_profiling::mojom::ProfilingService::AddProfilingClientCallback
refresh_callback = base::BindPostTaskToCurrentDefault(
base::BindOnce(&MemoryInternalsDOMHandler::RequestProcessList,
weak_factory_.GetWeakPtr(), callback_id.Clone()));
heap_profiling::Supervisor* supervisor =
heap_profiling::Supervisor::GetInstance();
if (supervisor->HasStarted()) {
supervisor->StartManualProfiling(pid, std::move(refresh_callback));
} else {
supervisor->Start(base::BindOnce(
&heap_profiling::Supervisor::StartManualProfiling,
base::Unretained(supervisor), pid, std::move(refresh_callback)));
}
}
void MemoryInternalsDOMHandler::OnJavascriptDisallowed() {
// Cancel any callbacks that might trigger Javascript.
weak_factory_.InvalidateWeakPtrs();
}
void MemoryInternalsDOMHandler::RequestProcessList(base::Value callback_id,
bool success) {
if (!success) {
return;
}
std::vector<base::Value::List> result;
// The only non-renderer child processes that currently support out-of-process
// heap profiling are GPU and UTILITY.
for (content::BrowserChildProcessHostIterator iter; !iter.Done(); ++iter) {
// Note that ChildProcessData.id is a child ID and not an OS PID.
const content::ChildProcessData& data = iter.GetData();
if (data.process_type == content::PROCESS_TYPE_GPU ||
data.process_type == content::PROCESS_TYPE_UTILITY) {
result.push_back(
MakeProcessInfo(data.GetProcess().Pid(), GetChildDescription(data)));
}
}
heap_profiling::Supervisor* supervisor =
heap_profiling::Supervisor::GetInstance();
// The supervisor hasn't started, so return an empty list.
if (!supervisor->HasStarted()) {
ReturnProcessListOnUIThread(std::move(callback_id), std::move(result),
std::vector<base::ProcessId>());
return;
}
supervisor->GetProfiledPids(base::BindOnce(
&MemoryInternalsDOMHandler::ReturnProcessListOnUIThread,
weak_factory_.GetWeakPtr(), std::move(callback_id), std::move(result)));
}
void MemoryInternalsDOMHandler::ReturnProcessListOnUIThread(
const base::Value& callback_id,
std::vector<base::Value::List> children,
std::vector<base::ProcessId> profiled_pids) {
// This function will be called with the child processes that are not
// renderers. It will fill in the browser and renderer processes on the UI
// thread (RenderProcessHost is UI-thread only) and return the full list.
base::Value::List process_list;
// Add browser process.
process_list.Append(MakeProcessInfo(base::GetCurrentProcId(), "Browser"));
// Append renderer processes.
auto iter = content::RenderProcessHost::AllHostsIterator();
while (!iter.IsAtEnd()) {
if (iter.GetCurrentValue()->GetProcess().IsValid()) {
base::ProcessId renderer_pid = iter.GetCurrentValue()->GetProcess().Pid();
if (renderer_pid != 0) {
// TODO(brettw) make a better description of the process, maybe see
// what TaskManager does to get the page title.
process_list.Append(MakeProcessInfo(renderer_pid, "Renderer"));
}
}
iter.Advance();
}
// Append all child processes collected on the IO thread.
for (auto& child : children) {
process_list.Append(std::move(child));
}
// Sort profiled_pids to allow binary_search in the loop.
std::sort(profiled_pids.begin(), profiled_pids.end());
// Append whether each process is being profiled.
for (base::Value& value : process_list) {
base::Value::List& list = value.GetList();
DCHECK_EQ(list.size(), 2u);
base::ProcessId pid = static_cast<base::ProcessId>(list[0].GetInt());
bool is_profiled =
std::binary_search(profiled_pids.begin(), profiled_pids.end(), pid);
list.Append(is_profiled);
}
// Pass the results in a dictionary.
base::Value::Dict result;
result.Set("message", GetMessageString());
result.Set("processes", std::move(process_list));
ResolveJavascriptCallback(callback_id, result);
}
void MemoryInternalsDOMHandler::FileSelected(const ui::SelectedFileInfo& file,
int index,
void* params) {
base::Value result("Saving...");
FireWebUIListener("save-dump-progress", result);
ProfilingProcessHost::GetInstance()->SaveTraceWithHeapDumpToFile(
file.path(),
base::BindOnce(&MemoryInternalsDOMHandler::SaveTraceFinished,
weak_factory_.GetWeakPtr()),
false);
select_file_dialog_ = nullptr;
}
void MemoryInternalsDOMHandler::FileSelectionCanceled(void* params) {
select_file_dialog_ = nullptr;
}
void MemoryInternalsDOMHandler::SaveTraceFinished(bool success) {
base::Value result(success ? "Save successful." : "Save failure.");
FireWebUIListener("save-dump-progress", result);
}
} // namespace
MemoryInternalsUI::MemoryInternalsUI(content::WebUI* web_ui)
: WebUIController(web_ui) {
web_ui->AddMessageHandler(
std::make_unique<MemoryInternalsDOMHandler>(web_ui));
CreateAndAddMemoryInternalsUIHTMLSource(Profile::FromWebUI(web_ui));
}
MemoryInternalsUI::~MemoryInternalsUI() {}