Compare commits

...

1 Commits

Author SHA1 Message Date
deepak1556
01130e5c9f feat: support samply profiler for packaged apps on macOS
https://github.com/mstange/samply is amazing to profile
Electron apps that need visualization of both JS and native
samples in an unified view. However, we cannot use the profiler
once the application is packaged due to restrictions from
entitlements and SIP that will disallow loading the preload library
from samply to exchange the task ports from the launched process.

This changes takes an alternative route inspired by crashpad architecture
where the samply profiler will be bundled as part of the application,
the browser process launches the profiler on demand and performs
mach port exchange via a simple handshake protocol implemented in
950b80e711
Every child process then launched by the browser will send their
task ports via the verified ipc connection the browser process
has created with the samply profiler process.

NB: the --samply-path is for testing purpose, we should remove it
and bundle the samply executable next to chrome_crashpad_handler
when packaging so that evil actors cannot hijack the ipc.
2026-01-31 01:37:57 +09:00
13 changed files with 905 additions and 1 deletions

View File

@@ -148,6 +148,10 @@ filenames = {
"shell/browser/mac/in_app_purchase_product.mm",
"shell/browser/mac/in_app_purchase.h",
"shell/browser/mac/in_app_purchase.mm",
"shell/browser/mac/samply_profiler_service.h",
"shell/browser/mac/samply_profiler_service.mm",
"shell/common/mac/samply_profiler_client.h",
"shell/common/mac/samply_profiler_client.mm",
"shell/browser/native_window_mac.h",
"shell/browser/native_window_mac.mm",
"shell/browser/notifications/mac/cocoa_notification.h",

View File

@@ -144,3 +144,4 @@ fix_linux_tray_id.patch
expose_gtk_ui_platform_field.patch
fix_os_crypt_async_cookie_encryption.patch
patch_osr_control_screen_info.patch
feat_support_passing_embedder_machrendezvous_ports_to_child.patch

View File

@@ -0,0 +1,196 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Sat, 31 Jan 2026 01:33:22 +0900
Subject: feat: support passing embedder machrendezvous ports to child
Used for samply profiler integration
diff --git a/base/apple/mach_port_rendezvous_mac.cc b/base/apple/mach_port_rendezvous_mac.cc
index a267d6efd2270f3d17e6c2e4ab10f26f77fd7bdf..94778ab1fe3f8f50e3e17f279fb7b0259ca19c9e 100644
--- a/base/apple/mach_port_rendezvous_mac.cc
+++ b/base/apple/mach_port_rendezvous_mac.cc
@@ -121,6 +121,8 @@ void MachPortRendezvousServerMac::RegisterPortsForPid(
lock_.AssertAcquired();
DCHECK_LT(ports.size(), internal::kMaximumRendezvousPorts);
DCHECK(!ports.empty());
+ VLOG(1) << "MachPortRendezvousServerMac::RegisterPortsForPid: pid=" << pid
+ << ", num_ports=" << ports.size();
ClientData& client = ClientDataForPid(pid);
CHECK(client.ports.empty());
@@ -149,9 +151,16 @@ MachPortRendezvousServerMac::ClientData::~ClientData() {
MachPortRendezvousServerMac::MachPortRendezvousServerMac() {
std::string bootstrap_name =
StringPrintf(kBootstrapNameFormat, apple::BaseBundleID(), getpid());
+ VLOG(1) << "MachPortRendezvousServerMac: Registering bootstrap service: "
+ << bootstrap_name;
kern_return_t kr = bootstrap_check_in(
bootstrap_port, bootstrap_name.c_str(),
apple::ScopedMachReceiveRight::Receiver(server_port_).get());
+ if (kr == KERN_SUCCESS) {
+ VLOG(1) << "MachPortRendezvousServerMac: bootstrap_check_in succeeded";
+ } else {
+ LOG(ERROR) << "MachPortRendezvousServerMac: bootstrap_check_in FAILED: " << kr;
+ }
BOOTSTRAP_CHECK(kr == KERN_SUCCESS, kr)
<< "bootstrap_check_in " << bootstrap_name;
dispatch_source_ = std::make_unique<apple::DispatchSource>(
@@ -159,6 +168,7 @@ MachPortRendezvousServerMac::MachPortRendezvousServerMac() {
HandleRequest();
});
dispatch_source_->Resume();
+ VLOG(1) << "MachPortRendezvousServerMac: Server ready, PID=" << getpid();
}
MachPortRendezvousServerMac::~MachPortRendezvousServerMac() = default;
@@ -245,6 +255,8 @@ bool MachPortRendezvousClientMac::AcquirePorts() {
apple::ScopedMachSendRight server_port;
std::string bootstrap_name = GetBootstrapName();
+ VLOG(1) << "MachPortRendezvousClientMac::AcquirePorts: PID=" << getpid()
+ << ", PPID=" << getppid() << ", looking up: " << bootstrap_name;
kern_return_t kr = bootstrap_look_up(
bootstrap_port, const_cast<char*>(bootstrap_name.c_str()),
apple::ScopedMachSendRight::Receiver(server_port).get());
@@ -252,6 +264,7 @@ bool MachPortRendezvousClientMac::AcquirePorts() {
BOOTSTRAP_LOG(ERROR, kr) << "bootstrap_look_up " << bootstrap_name;
return false;
}
+ VLOG(1) << "MachPortRendezvousClientMac::AcquirePorts: lookup succeeded";
mach_msg_id_t message_id = internal::kMachRendezvousMsgIdRequest;
size_t additional_data_size = 0;
diff --git a/base/process/launch.h b/base/process/launch.h
index 59eac5dc8c61b5c2df58d467149d4f3a9936611d..8b3622a57894d58eb193fdb6e4376578275df93a 100644
--- a/base/process/launch.h
+++ b/base/process/launch.h
@@ -40,6 +40,7 @@
#endif
#if BUILDFLAG(IS_MAC)
+#include "base/apple/scoped_mach_port.h"
#include "base/mac/process_requirement.h"
#endif
diff --git a/base/process/launch_mac.cc b/base/process/launch_mac.cc
index 8387fd7d2bcf8951b6cc024829c16d970799190c..c4681b8f9d4020039ef7907ffc8e8c1bf2882478 100644
--- a/base/process/launch_mac.cc
+++ b/base/process/launch_mac.cc
@@ -81,6 +81,7 @@ int posix_spawnattr_set_csm_np(const posix_spawnattr_t*, uint32_t)
#include "base/memory/raw_ptr.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/environment_internal.h"
+#include "base/synchronization/lock.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/thread_restrictions.h"
#include "base/trace_event/trace_event.h"
@@ -377,13 +378,12 @@ Process LaunchProcess(const std::vector<std::string>& argv,
int rv;
pid_t pid;
{
+#if !BUILDFLAG(IS_IOS_TVOS)
const bool has_mach_ports_for_rendezvous =
-#if BUILDFLAG(IS_IOS_TVOS)
- false
+ !options.mach_ports_for_rendezvous.empty();
#else
- !options.mach_ports_for_rendezvous.empty()
+ const bool has_mach_ports_for_rendezvous = false;
#endif // BUILDFLAG(IS_IOS_TVOS)
- ;
#if BUILDFLAG(IS_IOS)
// This code is only used for the iOS simulator to launch tests. We do not
diff --git a/content/browser/child_process_launcher_helper_mac.cc b/content/browser/child_process_launcher_helper_mac.cc
index cf9d9bfe7af12e16520354ebd1f7bc4050c57ec7..e91b1e69eec5a194f24287a61a77290b887503c1 100644
--- a/content/browser/child_process_launcher_helper_mac.cc
+++ b/content/browser/child_process_launcher_helper_mac.cc
@@ -20,6 +20,7 @@
#include "content/grit/content_resources.h"
#include "content/public/browser/child_process_launcher_utils.h"
#include "content/public/browser/content_browser_client.h"
+#include "content/public/common/content_client.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/result_codes.h"
@@ -106,6 +107,25 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
options->mach_ports_for_rendezvous.insert(std::make_pair(
'mojo', base::MachRendezvousPort(endpoint.TakeMachReceiveRight())));
+ VLOG(1) << "BeforeLaunchOnLauncherThread: PID=" << getpid()
+ << ", launching child process";
+
+ // Add any embedder-provided ports (e.g., for profiling)
+ base::MachPortsForRendezvous embedder_ports =
+ GetContentClient()->browser()->GetMachPortsForChildRendezvous();
+ VLOG(1) << "BeforeLaunchOnLauncherThread: Got " << embedder_ports.size()
+ << " embedder ports";
+ for (auto& port : embedder_ports) {
+ VLOG(1) << "BeforeLaunchOnLauncherThread: Adding embedder port key='"
+ << static_cast<char>(port.first >> 24)
+ << static_cast<char>((port.first >> 16) & 0xff)
+ << static_cast<char>((port.first >> 8) & 0xff)
+ << static_cast<char>(port.first & 0xff) << "'";
+ options->mach_ports_for_rendezvous.insert(std::move(port));
+ }
+ VLOG(1) << "BeforeLaunchOnLauncherThread: Total rendezvous ports: "
+ << options->mach_ports_for_rendezvous.size();
+
options->environment = delegate_->GetEnvironment();
options->clear_environment = !delegate_->ShouldInheritEnvironment();
options->current_directory = delegate_->GetCurrentDirectory();
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index b1e25f142beae77768128bb4467a3485c888a7ad..32d4b2a6817ba01d6d41f334400d0c0922ea0f79 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -23,6 +23,10 @@
#include "base/values.h"
#include "build/build_config.h"
#include "build/buildflag.h"
+
+#if BUILDFLAG(IS_MAC)
+#include "base/apple/mach_port_rendezvous.h"
+#endif
#include "components/language_detection/content/browser/content_language_detection_driver.h"
#include "components/language_detection/content/common/language_detection.mojom.h"
#include "components/language_detection/core/browser/language_detection_model_provider.h"
@@ -1748,6 +1752,11 @@ std::string ContentBrowserClient::GetChildProcessSuffix(int child_flags) {
NOTIMPLEMENTED();
return std::string();
}
+
+base::MachPortsForRendezvous
+ContentBrowserClient::GetMachPortsForChildRendezvous() {
+ return {};
+}
#endif
bool ContentBrowserClient::AreIsolatedWebAppsEnabled(
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index 185e0c86bc02a80121af3c213aa496ca62565c31..24fea64674476c182309697466d650b814a5d439 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -103,6 +103,10 @@
#include "base/posix/global_descriptors.h"
#endif
+#if BUILDFLAG(IS_MAC)
+#include "base/apple/mach_port_rendezvous.h"
+#endif
+
#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
#include "content/public/browser/posix_file_descriptor_info.h"
#endif
@@ -3032,6 +3036,12 @@ class CONTENT_EXPORT ContentBrowserClient {
// bundle should be placed next to the known //content Mac helpers in the
// framework bundle.
virtual std::string GetChildProcessSuffix(int child_flags);
+
+ // Returns Mach ports that should be passed to child processes via
+ // MachPortRendezvous. This allows the embedder to provide ports for
+ // services like profiling. The ports are copied (not moved) so the
+ // embedder retains ownership.
+ virtual base::MachPortsForRendezvous GetMachPortsForChildRendezvous();
#endif // BUILDFLAG(IS_MAC)
// Checks if Isolated Web Apps are enabled, e.g. by feature flag

View File

@@ -7,17 +7,20 @@
#include "shell/app/electron_library_main.h"
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_nsautorelease_pool.h"
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/i18n/icu_util.h"
#include "base/notreached.h"
#include "base/strings/sys_string_conversions.h"
#include "content/public/app/content_main.h"
#include "electron/fuses.h"
#include "shell/app/electron_main_delegate.h"
#include "shell/app/node_main.h"
#include "shell/common/electron_command_line.h"
#include "shell/common/mac/main_application_bundle.h"
#include "shell/common/mac/samply_profiler_client.h"
#include "uv.h"
int ElectronMain(int argc, char* argv[]) {
@@ -27,12 +30,15 @@ int ElectronMain(int argc, char* argv[]) {
electron::ElectronMainDelegate delegate;
// Ensure that Bundle Id is set before ContentMain.
// Ensure that Bundle Id is set before ContentMain AND before any code
// that uses MachPortRendezvousClient (like samply profiler connection).
// Refs https://chromium-review.googlesource.com/c/chromium/src/+/5581006
delegate.OverrideChildProcessPath();
delegate.OverrideFrameworkBundlePath();
delegate.SetUpBundleOverrides();
electron::MaybeConnectToSamplyProfiler();
return content::ContentMain(content::ContentMainParams{&delegate});
}
@@ -52,6 +58,21 @@ int ElectronInitializeICUandStartNode(int argc, char* argv[]) {
.Append("Contents")
.Append("Frameworks")
.Append(ELECTRON_PRODUCT_NAME " Framework.framework"));
// Set the bundle ID before trying to connect to samply profiler.
// This ensures MachPortRendezvousClient uses the correct service name.
@autoreleasepool {
NSBundle* bundle = electron::MainApplicationBundle();
std::string base_bundle_id =
base::SysNSStringToUTF8([bundle bundleIdentifier]);
NSString* team_id = [bundle objectForInfoDictionaryKey:@"ElectronTeamID"];
if (team_id)
base_bundle_id = base::SysNSStringToUTF8(team_id) + "." + base_bundle_id;
base::apple::SetBaseBundleIDOverride(base_bundle_id);
}
electron::MaybeConnectToSamplyProfiler();
base::i18n::InitializeICU();
return electron::NodeMain();
}

View File

@@ -71,11 +71,17 @@ void ElectronMainDelegate::OverrideChildProcessPath() {
void ElectronMainDelegate::SetUpBundleOverrides() {
@autoreleasepool {
NSBundle* bundle = MainApplicationBundle();
VLOG(1) << "SetUpBundleOverrides: bundle path="
<< (bundle ? base::SysNSStringToUTF8([bundle bundlePath]) : "nil");
std::string base_bundle_id =
base::SysNSStringToUTF8([bundle bundleIdentifier]);
VLOG(1) << "SetUpBundleOverrides: bundle id from Info.plist="
<< (base_bundle_id.empty() ? "(empty)" : base_bundle_id);
NSString* team_id = [bundle objectForInfoDictionaryKey:@"ElectronTeamID"];
if (team_id)
base_bundle_id = base::SysNSStringToUTF8(team_id) + "." + base_bundle_id;
VLOG(1) << "SetUpBundleOverrides: Setting BaseBundleID to: "
<< base_bundle_id;
base::apple::SetBaseBundleIDOverride(base_bundle_id);
}
}

View File

@@ -136,7 +136,9 @@
#elif BUILDFLAG(IS_WIN)
#include "net/ssl/client_cert_store_win.h"
#elif BUILDFLAG(IS_MAC)
#include "base/apple/mach_port_rendezvous.h"
#include "net/ssl/client_cert_store_mac.h"
#include "shell/browser/mac/samply_profiler_service.h"
#elif defined(USE_OPENSSL)
#include "net/ssl/client_cert_store.h"
#endif
@@ -1807,6 +1809,28 @@ device::GeolocationSystemPermissionManager*
ElectronBrowserClient::GetGeolocationSystemPermissionManager() {
return device::GeolocationSystemPermissionManager::GetInstance();
}
base::MachPortsForRendezvous
ElectronBrowserClient::GetMachPortsForChildRendezvous() {
VLOG(1) << "GetMachPortsForChildRendezvous() called, PID=" << getpid();
base::MachPortsForRendezvous ports;
auto* profiler = SamplyProfilerService::GetInstance();
VLOG(1) << "GetMachPortsForChildRendezvous: profiler="
<< (profiler ? "yes" : "null");
if (profiler) {
mach_port_t port = profiler->GetProfilerPort();
VLOG(1) << "GetMachPortsForChildRendezvous: profiler port=" << port;
if (port != MACH_PORT_NULL) {
ports.emplace(kMachPortKeyProfiler,
base::MachRendezvousPort(port, MACH_MSG_TYPE_COPY_SEND));
VLOG(1) << "GetMachPortsForChildRendezvous: Added profiler port to "
"rendezvous";
}
}
VLOG(1) << "GetMachPortsForChildRendezvous: returning " << ports.size()
<< " ports";
return ports;
}
#endif
content::HidDelegate* ElectronBrowserClient::GetHidDelegate() {

View File

@@ -120,6 +120,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
#if BUILDFLAG(IS_MAC)
device::GeolocationSystemPermissionManager*
GetGeolocationSystemPermissionManager() override;
base::MachPortsForRendezvous GetMachPortsForChildRendezvous() override;
#endif
content::PlatformNotificationService* GetPlatformNotificationService();

View File

@@ -102,6 +102,7 @@
#if BUILDFLAG(IS_MAC)
#include "components/os_crypt/common/keychain_password_mac.h"
#include "shell/browser/mac/samply_profiler_service.h"
#include "shell/browser/ui/cocoa/views_delegate_mac.h"
#else
#include "shell/browser/ui/views/electron_views_delegate.h"
@@ -284,6 +285,12 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
}
int ElectronBrowserMainParts::PreCreateThreads() {
#if BUILDFLAG(IS_MAC)
// Initialize samply profiling very early so child processes can connect.
// This must be before any child processes are spawned.
SamplyProfilerService::Initialize();
#endif
if (!views::LayoutProvider::Get()) {
layout_provider_ = std::make_unique<views::LayoutProvider>();
}
@@ -552,6 +559,8 @@ void ElectronBrowserMainParts::PostCreateMainMessageLoop() {
void ElectronBrowserMainParts::PostMainMessageLoopRun() {
#if BUILDFLAG(IS_MAC)
FreeAppDelegate();
// Shutdown samply profiling and save the profile.
SamplyProfilerService::Shutdown();
#endif
// Shutdown the DownloadManager before destroying Node to prevent

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2025 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_MAC_SAMPLY_PROFILER_SERVICE_H_
#define ELECTRON_SHELL_BROWSER_MAC_SAMPLY_PROFILER_SERVICE_H_
#include <mach/mach.h>
#include <memory>
#include <string>
#include "base/apple/scoped_mach_port.h"
#include "base/files/file_path.h"
#include "base/process/process.h"
#include "shell/common/mac/samply_profiler_types.h"
namespace electron {
// Manages the samply profiler connection for the browser process.
//
// When profiling is enabled, this service:
// 1. Creates a Mach receive port for task connections
// 2. Launches the bundled samply process with --handshake-fd
// 3. Sends the receive right to samply via the handshake protocol
// 4. Provides the send right for child processes via MachPortRendezvous
// 5. Connects the browser process itself to the profiler
class SamplyProfilerService {
public:
// Returns the singleton instance. Returns nullptr if profiling is not
// enabled.
static SamplyProfilerService* GetInstance();
// Initialize profiling. Must be called early in browser startup.
// Returns true if profiling was successfully started.
static bool Initialize();
// Shutdown profiling and wait for samply to write the profile.
static void Shutdown();
SamplyProfilerService(const SamplyProfilerService&) = delete;
SamplyProfilerService& operator=(const SamplyProfilerService&) = delete;
bool IsEnabled() const { return is_enabled_; }
// Returns a send right to the profiler port (for child process rendezvous).
mach_port_t GetProfilerPort() const {
return send_right_.is_valid() ? send_right_.get() : MACH_PORT_NULL;
}
private:
SamplyProfilerService(const base::FilePath& samply_path,
const base::FilePath& output_path);
~SamplyProfilerService();
// Perform the handshake with samply to transfer the receive right.
bool PerformHandshake(int write_fd);
// Connect browser process to the profiler.
void ConnectSelfToProfiler();
// The receive right for task connections
// (owned temporarily during handshake)
base::apple::ScopedMachReceiveRight receive_port_;
// Send right to the profiler port
base::apple::ScopedMachSendRight send_right_;
// The samply process handle
base::Process samply_process_;
// Path to the samply executable
base::FilePath samply_path_;
// Path to where samply should save the profile
base::FilePath output_path_;
// Whether profiling is active
bool is_enabled_ = false;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_MAC_SAMPLY_PROFILER_SERVICE_H_

View File

@@ -0,0 +1,334 @@
// Copyright (c) 2025 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/mac/samply_profiler_service.h"
#include <servers/bootstrap.h>
#include <unistd.h>
#include "base/apple/bundle_locations.h"
#include "base/apple/mach_logging.h"
#include "base/apple/scoped_mach_port.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/numerics/byte_conversions.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/rand_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "shell/common/mac/samply_profiler_client.h"
namespace electron {
namespace {
SamplyProfilerService* g_instance = nullptr;
// Returns the default path to the samply executable in the app bundle
base::FilePath GetDefaultSamplyPath() {
base::FilePath framework_path = base::apple::FrameworkBundlePath();
return framework_path.Append("Helpers").Append("samply");
}
// Returns the default output path for the profile.
base::FilePath GetDefaultOutputPath() {
base::FilePath temp_dir;
if (!base::GetTempDir(&temp_dir)) {
temp_dir = base::FilePath("/tmp");
}
return temp_dir.Append("electron_profile.json.gz");
}
} // namespace
// static
SamplyProfilerService* SamplyProfilerService::GetInstance() {
return g_instance;
}
// static
bool SamplyProfilerService::Initialize() {
VLOG(1) << "SamplyProfilerService::Initialize() called, PID=" << getpid();
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
if (!command_line->HasSwitch("enable-samply-profiling")) {
return false;
}
if (g_instance) {
VLOG(1) << "SamplyProfilerService: Already initialized, enabled="
<< g_instance->IsEnabled();
return g_instance->IsEnabled();
}
base::FilePath samply_path;
if (command_line->HasSwitch("samply-path")) {
samply_path = command_line->GetSwitchValuePath("samply-path");
} else {
samply_path = GetDefaultSamplyPath();
}
if (!base::PathExists(samply_path)) {
LOG(ERROR) << "samply not found at: " << samply_path;
return false;
}
base::FilePath output_path;
if (command_line->HasSwitch("samply-output-path")) {
output_path = command_line->GetSwitchValuePath("samply-output-path");
} else {
output_path = GetDefaultOutputPath();
}
VLOG(1) << "SamplyProfilerService: Creating new instance...";
g_instance = new SamplyProfilerService(samply_path, output_path);
VLOG(1) << "SamplyProfilerService: Created, enabled="
<< g_instance->IsEnabled()
<< ", port=" << g_instance->GetProfilerPort();
return g_instance->IsEnabled();
}
// static
void SamplyProfilerService::Shutdown() {
if (g_instance) {
delete g_instance;
g_instance = nullptr;
}
}
SamplyProfilerService::SamplyProfilerService(const base::FilePath& samply_path,
const base::FilePath& output_path)
: samply_path_(samply_path), output_path_(output_path) {
mach_port_t port;
kern_return_t kr =
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_port_allocate for profiler";
return;
}
receive_port_.reset(port);
mach_port_t send_right;
mach_msg_type_name_t acquired_type;
kr = mach_port_extract_right(mach_task_self(), receive_port_.get(),
MACH_MSG_TYPE_MAKE_SEND, &send_right,
&acquired_type);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_port_extract_right for profiler";
return;
}
send_right_.reset(send_right);
int pipe_fds[2];
if (pipe(pipe_fds) != 0) {
PLOG(ERROR) << "pipe for samply handshake";
return;
}
int read_fd = pipe_fds[0];
int write_fd = pipe_fds[1];
base::LaunchOptions options;
options.fds_to_remap.emplace_back(read_fd, read_fd);
std::vector<std::string> argv = {
samply_path_.value(), "record",
"--handshake-fd", base::StringPrintf("%d", read_fd),
"--output", output_path_.value()};
samply_process_ = base::LaunchProcess(argv, options);
close(read_fd);
if (!samply_process_.IsValid()) {
LOG(ERROR) << "Failed to launch samply";
close(write_fd);
return;
}
if (!PerformHandshake(write_fd)) {
LOG(ERROR) << "Handshake with samply failed";
samply_process_.Terminate(0, false);
return;
}
ConnectSelfToProfiler();
is_enabled_ = true;
VLOG(1) << "Samply profiling enabled, PID: " << samply_process_.Pid();
}
SamplyProfilerService::~SamplyProfilerService() {
// Release our send right - this triggers MACH_NOTIFY_NO_SENDERS in samply
// when all other clients have disconnected, causing it to save and exit
send_right_.reset();
if (samply_process_.IsValid()) {
// Send SIGINT to samply to request a graceful shutdown and
// save the profile.
VLOG(1) << "Sending SIGINT to samply to save profile...";
kill(samply_process_.Pid(), SIGINT);
// Wait briefly for samply to save the profile and exit.
int exit_code;
if (!samply_process_.WaitForExitWithTimeout(base::Seconds(5), &exit_code)) {
LOG(WARNING) << "Samply did not exit in time, sending SIGTERM";
kill(samply_process_.Pid(), SIGTERM);
samply_process_.WaitForExitWithTimeout(base::Seconds(2), &exit_code);
}
VLOG(1) << "Samply exited with code: " << exit_code;
}
}
bool SamplyProfilerService::PerformHandshake(int write_fd) {
// Generate a random token for authentication
uint64_t token = base::RandUint64();
// Generate a unique service name
std::string service_name = base::StringPrintf("org.electron.samply.%d.%llu",
getpid(), base::RandUint64());
// Register the receive port with the bootstrap server
mach_port_t local_bootstrap_port;
kern_return_t kr = task_get_special_port(
mach_task_self(), TASK_BOOTSTRAP_PORT, &local_bootstrap_port);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "task_get_special_port";
close(write_fd);
return false;
}
// Extract a send right to register with bootstrap
mach_port_t service_port;
mach_msg_type_name_t acquired_type;
kr = mach_port_extract_right(mach_task_self(), receive_port_.get(),
MACH_MSG_TYPE_MAKE_SEND, &service_port,
&acquired_type);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_port_extract_right for service";
close(write_fd);
return false;
}
// Register with bootstrap server
// bootstrap_register is deprecated but there's no modern replacement for
// this functionality. We use it to allow samply to look up our port.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
kr =
bootstrap_register(local_bootstrap_port,
const_cast<char*>(service_name.c_str()), service_port);
#pragma clang diagnostic pop
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "bootstrap_register";
close(write_fd);
mach_port_deallocate(mach_task_self(), service_port);
return false;
}
if (write(write_fd, &token, sizeof(token)) != sizeof(token)) {
PLOG(ERROR) << "write token";
close(write_fd);
return false;
}
uint32_t name_len = static_cast<uint32_t>(service_name.size());
if (write(write_fd, &name_len, sizeof(name_len)) != sizeof(name_len)) {
PLOG(ERROR) << "write name_len";
close(write_fd);
return false;
}
if (write(write_fd, service_name.c_str(), name_len) !=
static_cast<ssize_t>(name_len)) {
PLOG(ERROR) << "write service_name";
close(write_fd);
return false;
}
close(write_fd);
// Wait for samply to send its check-in message
CheckInMessageBuffer check_in = {};
check_in.header.msgh_size = sizeof(CheckInMessageBuffer);
check_in.header.msgh_local_port = receive_port_.get();
kr = mach_msg(&check_in.header, MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0,
sizeof(CheckInMessageBuffer), receive_port_.get(),
10000, // 10 second timeout
MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_msg receive check-in from samply";
return false;
}
VLOG(1) << "Received check-in message: size=" << check_in.header.msgh_size
<< " remote_port=" << check_in.header.msgh_remote_port
<< " id=" << check_in.header.msgh_id;
// Validate message size before accessing fields.
if (check_in.header.msgh_size < kCheckInMessageMinSize) {
LOG(ERROR) << "Check-in message too small, size="
<< check_in.header.msgh_size
<< " expected >= " << kCheckInMessageMinSize;
return false;
}
// Extract the token from the received message at known offset.
uint64_t received_token = base::U64FromNativeEndian(
base::span(check_in.bytes)
.subspan<kCheckInTokenOffset, sizeof(uint64_t)>());
if (received_token != token) {
LOG(ERROR) << "Invalid token in samply check-in message";
return false;
}
// Send reply containing the receive right
// After this, samply owns the receive right and we only keep a send right
CheckInReply reply = {};
// Use MACH_MSG_TYPE_MOVE_SEND because samply created a regular send right
// (via MACH_MSG_TYPE_MAKE_SEND), not a send-once right.
reply.header.msgh_bits =
MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, 0);
reply.header.msgh_size = sizeof(CheckInReply);
reply.header.msgh_remote_port = check_in.header.msgh_remote_port;
reply.header.msgh_local_port = MACH_PORT_NULL;
reply.header.msgh_id = check_in.header.msgh_id + 100;
reply.body.msgh_descriptor_count = 1;
reply.port.name = receive_port_.release(); // Transfer ownership to samply
reply.port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE;
reply.port.type = MACH_MSG_PORT_DESCRIPTOR;
kr = mach_msg(&reply.header, MACH_SEND_MSG, sizeof(CheckInReply), 0,
MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_msg send check-in reply to samply";
return false;
}
VLOG(1) << "Handshake with samply completed successfully";
return true;
}
void SamplyProfilerService::ConnectSelfToProfiler() {
if (!send_right_.is_valid()) {
LOG(WARNING) << "No send right available for self-connection";
return;
}
if (ConnectToSamplyProfiler(send_right_.get())) {
VLOG(1) << "Browser process connected to samply profiler";
} else {
LOG(WARNING) << "Failed to connect browser process to samply profiler";
}
}
} // namespace electron

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2025 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_MAC_SAMPLY_PROFILER_CLIENT_H_
#define ELECTRON_SHELL_COMMON_MAC_SAMPLY_PROFILER_CLIENT_H_
#include <mach/mach.h>
namespace electron {
// Attempts to connect this process to the samply profiler.
// Should be called early in process startup.
// Uses MachPortRendezvous to get the profiler port from the parent process.
void MaybeConnectToSamplyProfiler();
// Connects this process to the samply profiler using the given send right.
// The send right is borrowed (not consumed).
// Returns true if connection was successful.
bool ConnectToSamplyProfiler(mach_port_t profiler_port);
} // namespace electron
#endif // ELECTRON_SHELL_COMMON_MAC_SAMPLY_PROFILER_CLIENT_H_

View File

@@ -0,0 +1,118 @@
// Copyright (c) 2025 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/mac/samply_profiler_client.h"
#include <unistd.h>
#include "base/apple/mach_logging.h"
#include "base/apple/mach_port_rendezvous.h"
#include "base/apple/scoped_mach_port.h"
#include "base/containers/span.h"
#include "base/logging.h"
#include "base/numerics/byte_conversions.h"
#include "shell/common/mac/samply_profiler_types.h"
namespace electron {
bool ConnectToSamplyProfiler(mach_port_t profiler_port) {
if (profiler_port == MACH_PORT_NULL) {
return false;
}
base::apple::ScopedMachReceiveRight reply_port;
kern_return_t kr = mach_port_allocate(
mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
base::apple::ScopedMachReceiveRight::Receiver(reply_port).get());
if (kr != KERN_SUCCESS) {
MACH_LOG(WARNING, kr) << "mach_port_allocate for profiler reply";
return false;
}
SamplyMessage msg = {};
msg.header.msgh_bits =
MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
msg.header.msgh_size = sizeof(SamplyMessage);
msg.header.msgh_remote_port = profiler_port;
msg.header.msgh_local_port = MACH_PORT_NULL;
msg.header.msgh_id = 0;
msg.body.msgh_descriptor_count = 2;
msg.reply_port.name = reply_port.get();
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.task_port.name = mach_task_self();
msg.task_port.disposition = MACH_MSG_TYPE_COPY_SEND;
msg.task_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.magic = kSamplyMagic;
msg.pid = static_cast<uint32_t>(getpid());
kr = mach_msg(&msg.header, MACH_SEND_MSG, sizeof(SamplyMessage), 0,
MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
MACH_LOG(WARNING, kr) << "mach_msg send to samply profiler";
return false;
}
SamplyReplyBuffer reply = {};
reply.header.msgh_size = sizeof(SamplyReplyBuffer);
reply.header.msgh_local_port = reply_port.get();
kr = mach_msg(&reply.header, MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0,
sizeof(SamplyReplyBuffer), reply_port.get(), 5000,
MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
MACH_LOG(WARNING, kr) << "mach_msg receive from samply profiler";
return false;
}
if (reply.header.msgh_size < kSamplyReplyMinSize) {
LOG(ERROR) << "Samply profiler: reply too small, size="
<< reply.header.msgh_size
<< " expected >= " << kSamplyReplyMinSize;
return false;
}
// Read magic and status from bytes[] at known offsets.
uint32_t magic = base::U32FromNativeEndian(
base::span(reply.bytes)
.subspan<kSamplyReplyMagicOffset, sizeof(uint32_t)>());
uint32_t status = base::U32FromNativeEndian(
base::span(reply.bytes)
.subspan<kSamplyReplyStatusOffset, sizeof(uint32_t)>());
if (magic != kSamplyMagic) {
LOG(ERROR) << "Samply profiler: invalid reply magic";
return false;
}
if (status != 0) {
LOG(ERROR) << "Samply profiler: error status " << status;
return false;
}
return true;
}
void MaybeConnectToSamplyProfiler() {
auto* client = base::MachPortRendezvousClient::GetInstance();
if (!client) {
// Not available (e.g., no rendezvous server or running standalone)
return;
}
auto profiler_port = client->TakeSendRight(kMachPortKeyProfiler);
if (!profiler_port.is_valid()) {
return;
}
if (ConnectToSamplyProfiler(profiler_port.get())) {
VLOG(1) << "Connected to samply profiler (child process)";
}
}
} // namespace electron

View File

@@ -0,0 +1,81 @@
// Copyright (c) 2025 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_MAC_SAMPLY_PROFILER_TYPES_H_
#define ELECTRON_SHELL_COMMON_MAC_SAMPLY_PROFILER_TYPES_H_
#include <mach/mach.h>
#include <stdint.h>
namespace electron {
// Key for the profiler port in MachPortRendezvous.
inline constexpr uint32_t kMachPortKeyProfiler = 0x70726F66;
// Magic number for samply protocol messages.
inline constexpr uint32_t kSamplyMagic = 0x534D504C;
// Expected minimum size for SamplyReply from Rust.
inline constexpr size_t kSamplyReplyMinSize = 36;
// Expected minimum size for CheckIn message from Rust.
inline constexpr size_t kCheckInMessageMinSize = 48;
// Byte offsets for reading fields from raw message buffers.
inline constexpr size_t kSamplyReplyMagicOffset =
sizeof(mach_msg_header_t) + sizeof(mach_msg_body_t);
inline constexpr size_t kSamplyReplyStatusOffset =
kSamplyReplyMagicOffset + sizeof(uint32_t);
inline constexpr size_t kCheckInTokenOffset =
sizeof(mach_msg_header_t) + sizeof(mach_msg_body_t) +
sizeof(mach_msg_port_descriptor_t);
// Message structure for connecting a process to samply.
// Matches SimpleSamplyMessage in samply's simple_server.rs.
struct SamplyMessage {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t reply_port;
mach_msg_port_descriptor_t task_port;
uint32_t magic;
uint32_t pid;
};
// Reply message structure from samply.
// Uses a union to provide both structured access and raw byte access.
union SamplyReplyBuffer {
mach_msg_header_t header;
struct {
mach_msg_header_t header;
mach_msg_body_t body;
uint32_t magic;
uint32_t status;
} structured;
uint8_t bytes[128];
};
// Buffer for receiving check-in message from samply during handshake.
// Uses a union to provide both structured access and raw byte access.
union CheckInMessageBuffer {
mach_msg_header_t header;
struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t port;
uint64_t token;
} structured;
uint8_t bytes[256];
};
// Reply message sent to samply containing the receive right.
// Matches ChildPortCheckInReply in samply's simple_server.rs.
struct CheckInReply {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t port;
};
} // namespace electron
#endif // ELECTRON_SHELL_COMMON_MAC_SAMPLY_PROFILER_TYPES_H_