Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "ProfilerChild.h"
#include "GeckoProfiler.h"
#include "platform.h"
#include "ProfilerCodeAddressService.h"
#include "ProfilerControl.h"
#include "ProfilerParent.h"
#include "chrome/common/ipc_channel.h"
#include "nsPrintfCString.h"
#include "nsThreadUtils.h"
#include <memory>
namespace mozilla {
MOZ_RUNINIT /* static */ DataMutexBase<ProfilerChild::ProfilerChildAndUpdate,
baseprofiler::detail::BaseProfilerMutex>
ProfilerChild::sPendingChunkManagerUpdate{
"ProfilerChild::sPendingChunkManagerUpdate"};
ProfilerChild::ProfilerChild()
: mThread(NS_GetCurrentThread()), mDestroyed(false) {
MOZ_COUNT_CTOR(ProfilerChild);
}
ProfilerChild::~ProfilerChild() { MOZ_COUNT_DTOR(ProfilerChild); }
void ProfilerChild::ResolveChunkUpdate(
PProfilerChild::AwaitNextChunkManagerUpdateResolver& aResolve) {
MOZ_ASSERT(!!aResolve,
"ResolveChunkUpdate should only be called when there's a pending "
"resolver");
MOZ_ASSERT(
!mChunkManagerUpdate.IsNotUpdate(),
"ResolveChunkUpdate should only be called with a real or final update");
MOZ_ASSERT(
!mDestroyed,
"ResolveChunkUpdate should not be called if the actor was destroyed");
if (mChunkManagerUpdate.IsFinal()) {
// Final update, send a special "unreleased value", but don't clear the
// local copy so we know we got the final update.
std::move(aResolve)(ProfilerParent::MakeFinalUpdate());
} else {
// Optimization note: The ProfileBufferChunkManagerUpdate constructor takes
// the newly-released chunks nsTArray by reference-to-const, therefore
// constructing and then moving the array here would make a copy. So instead
// we first give it an empty array, and then we can write the data directly
// into the update's array.
ProfileBufferChunkManagerUpdate update{
mChunkManagerUpdate.UnreleasedBytes(),
mChunkManagerUpdate.ReleasedBytes(),
mChunkManagerUpdate.OldestDoneTimeStamp(),
{}};
update.newlyReleasedChunks().SetCapacity(
mChunkManagerUpdate.NewlyReleasedChunksRef().size());
for (const ProfileBufferControlledChunkManager::ChunkMetadata& chunk :
mChunkManagerUpdate.NewlyReleasedChunksRef()) {
update.newlyReleasedChunks().EmplaceBack(chunk.mDoneTimeStamp,
chunk.mBufferBytes);
}
std::move(aResolve)(update);
// Clear the update we just sent, so it's ready for later updates to be
// folded into it.
mChunkManagerUpdate.Clear();
}
// Discard the resolver, so it's empty next time there's a new request.
aResolve = nullptr;
}
void ProfilerChild::ProcessChunkManagerUpdate(
ProfileBufferControlledChunkManager::Update&& aUpdate) {
if (mDestroyed) {
return;
}
// Always store the data, it could be the final update.
mChunkManagerUpdate.Fold(std::move(aUpdate));
if (mAwaitNextChunkManagerUpdateResolver) {
// There is already a pending resolver, give it the info now.
ResolveChunkUpdate(mAwaitNextChunkManagerUpdateResolver);
}
}
/* static */ void ProfilerChild::ProcessPendingUpdate() {
auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
if (!lockedUpdate->mProfilerChild || lockedUpdate->mUpdate.IsNotUpdate()) {
return;
}
lockedUpdate->mProfilerChild->mThread->Dispatch(NS_NewRunnableFunction(
"ProfilerChild::ProcessPendingUpdate", []() mutable {
auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
if (!lockedUpdate->mProfilerChild ||
lockedUpdate->mUpdate.IsNotUpdate()) {
return;
}
lockedUpdate->mProfilerChild->ProcessChunkManagerUpdate(
std::move(lockedUpdate->mUpdate));
lockedUpdate->mUpdate.Clear();
}));
}
/* static */ bool ProfilerChild::IsLockedOnCurrentThread() {
return sPendingChunkManagerUpdate.Mutex().IsLockedOnCurrentThread();
}
void ProfilerChild::SetupChunkManager() {
mChunkManager = profiler_get_controlled_chunk_manager();
if (NS_WARN_IF(!mChunkManager)) {
return;
}
// Make sure there are no updates (from a previous run).
mChunkManagerUpdate.Clear();
{
auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
lockedUpdate->mProfilerChild = this;
lockedUpdate->mUpdate.Clear();
}
mChunkManager->SetUpdateCallback(
[](ProfileBufferControlledChunkManager::Update&& aUpdate) {
// Updates from the chunk manager are stored for later processing.
// We avoid dispatching a task, as this could deadlock (if the queueing
// mutex is held elsewhere).
auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
if (!lockedUpdate->mProfilerChild) {
return;
}
lockedUpdate->mUpdate.Fold(std::move(aUpdate));
});
}
void ProfilerChild::ResetChunkManager() {
if (!mChunkManager) {
return;
}
// We have a chunk manager, reset the callback, which will add a final
// pending update.
mChunkManager->SetUpdateCallback({});
// Clear the pending update.
auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
lockedUpdate->mProfilerChild = nullptr;
lockedUpdate->mUpdate.Clear();
// And process a final update right now.
ProcessChunkManagerUpdate(
ProfileBufferControlledChunkManager::Update(nullptr));
mChunkManager = nullptr;
mAwaitNextChunkManagerUpdateResolver = nullptr;
}
mozilla::ipc::IPCResult ProfilerChild::RecvStart(
const ProfilerInitParams& params, StartResolver&& aResolve) {
nsTArray<const char*> filterArray;
for (size_t i = 0; i < params.filters().Length(); ++i) {
filterArray.AppendElement(params.filters()[i].get());
}
profiler_start(PowerOfTwo32(params.entries()), params.interval(),
params.features(), filterArray.Elements(),
filterArray.Length(), params.activeTabID(), params.duration());
SetupChunkManager();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvEnsureStarted(
const ProfilerInitParams& params, EnsureStartedResolver&& aResolve) {
nsTArray<const char*> filterArray;
for (size_t i = 0; i < params.filters().Length(); ++i) {
filterArray.AppendElement(params.filters()[i].get());
}
profiler_ensure_started(PowerOfTwo32(params.entries()), params.interval(),
params.features(), filterArray.Elements(),
filterArray.Length(), params.activeTabID(),
params.duration());
SetupChunkManager();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvStop(StopResolver&& aResolve) {
ResetChunkManager();
profiler_stop();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvPause(PauseResolver&& aResolve) {
profiler_pause();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvResume(ResumeResolver&& aResolve) {
profiler_resume();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvPauseSampling(
PauseSamplingResolver&& aResolve) {
profiler_pause_sampling();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvResumeSampling(
ResumeSamplingResolver&& aResolve) {
profiler_resume_sampling();
aResolve(/* unused */ true);
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvWaitOnePeriodicSampling(
WaitOnePeriodicSamplingResolver&& aResolve) {
std::shared_ptr<WaitOnePeriodicSamplingResolver> resolve =
std::make_shared<WaitOnePeriodicSamplingResolver>(std::move(aResolve));
if (!profiler_callback_after_sampling(
[self = RefPtr(this), resolve](SamplingState aSamplingState) mutable {
if (self->mDestroyed) {
return;
}
MOZ_RELEASE_ASSERT(self->mThread);
self->mThread->Dispatch(NS_NewRunnableFunction(
"nsProfiler::WaitOnePeriodicSampling result on main thread",
[resolve = std::move(resolve), aSamplingState]() {
(*resolve)(aSamplingState ==
SamplingState::SamplingCompleted ||
aSamplingState ==
SamplingState::NoStackSamplingCompleted);
}));
})) {
// Callback was not added (e.g., profiler is not running) and will never be
// invoked, so we need to resolve the promise here.
(*resolve)(false);
}
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvClearAllPages() {
profiler_clear_all_pages();
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvAwaitNextChunkManagerUpdate(
AwaitNextChunkManagerUpdateResolver&& aResolve) {
MOZ_ASSERT(!mDestroyed,
"Recv... should not be called if the actor was destroyed");
// Pick up pending updates if any.
{
auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
if (lockedUpdate->mProfilerChild && !lockedUpdate->mUpdate.IsNotUpdate()) {
mChunkManagerUpdate.Fold(std::move(lockedUpdate->mUpdate));
lockedUpdate->mUpdate.Clear();
}
}
if (mChunkManagerUpdate.IsNotUpdate()) {
// No data yet, store the resolver for later.
mAwaitNextChunkManagerUpdateResolver = std::move(aResolve);
} else {
// We have data, send it now.
ResolveChunkUpdate(aResolve);
}
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvDestroyReleasedChunksAtOrBefore(
const TimeStamp& aTimeStamp) {
if (mChunkManager) {
mChunkManager->DestroyChunksAtOrBefore(aTimeStamp);
}
return IPC_OK();
}
struct GatherProfileThreadParameters
: public external::AtomicRefCounted<GatherProfileThreadParameters> {
MOZ_DECLARE_REFCOUNTED_TYPENAME(GatherProfileThreadParameters)
GatherProfileThreadParameters(
RefPtr<ProfilerChild> aProfilerChild,
RefPtr<ProgressLogger::SharedProgress> aProgress,
ProfilerChild::GatherProfileResolver&& aResolver)
: profilerChild(std::move(aProfilerChild)),
progress(std::move(aProgress)),
resolver(std::move(aResolver)) {}
RefPtr<ProfilerChild> profilerChild;
FailureLatchSource failureLatchSource;
// Separate RefPtr used when working on separate thread. This way, if the
// "ProfilerChild" thread decides to overwrite its mGatherProfileProgress with
// a new one, the work done here will still only use the old one.
RefPtr<ProgressLogger::SharedProgress> progress;
// Resolver for the GatherProfile promise. Must only be called on the
// "ProfilerChild" thread.
ProfilerChild::GatherProfileResolver resolver;
};
/* static */
void ProfilerChild::GatherProfileThreadFunction(
void* already_AddRefedParameters) {
PR_SetCurrentThreadName("GatherProfileThread");
RefPtr<GatherProfileThreadParameters> parameters =
already_AddRefed<GatherProfileThreadParameters>{
static_cast<GatherProfileThreadParameters*>(
already_AddRefedParameters)};
ProgressLogger progressLogger(
parameters->progress, "Gather-profile thread started", "Profile sent");
using namespace mozilla::literals::ProportionValue_literals; // For `1_pc`.
auto writer =
MakeUnique<SpliceableChunkedJSONWriter>(parameters->failureLatchSource);
if (!profiler_get_profile_json(
*writer,
/* aSinceTime */ 0,
/* aIsShuttingDown */ false,
progressLogger.CreateSubLoggerFromTo(
1_pc, "profiler_get_profile_json started", 99_pc,
"profiler_get_profile_json done"))) {
// Failed to get a profile, reset the writer pointer, so that we'll send a
// failure message.
writer.reset();
}
if (NS_WARN_IF(NS_FAILED(
parameters->profilerChild->mThread->Dispatch(NS_NewRunnableFunction(
"ProfilerChild::ProcessPendingUpdate",
[parameters,
// Forward progress logger to on-ProfilerChild-thread task, so
// that it doesn't get marked as 100% done when this off-thread
// function ends.
progressLogger = std::move(progressLogger),
writer = std::move(writer)]() mutable {
// We are now on the ProfilerChild thread, about to send the
// completed profile. Any incoming progress request will now be
// handled after this task ends, so updating the progress is now
// useless and we can just get rid of the progress storage.
if (parameters->profilerChild->mGatherProfileProgress ==
parameters->progress) {
// The ProfilerChild progress is still the one we know.
parameters->profilerChild->mGatherProfileProgress = nullptr;
}
// Shmem allocation and promise resolution must be made on the
// ProfilerChild thread, that's why this task was needed here.
mozilla::ipc::Shmem shmem;
if (writer) {
if (const size_t len = writer->ChunkedWriteFunc().Length();
len < UINT32_MAX) {
bool shmemSuccess = true;
const bool copySuccess =
writer->ChunkedWriteFunc()
.CopyDataIntoLazilyAllocatedBuffer(
[&](size_t allocationSize) -> char* {
MOZ_ASSERT(allocationSize == len + 1);
if (parameters->profilerChild->AllocShmem(
allocationSize, &shmem)) {
return shmem.get<char>();
}
shmemSuccess = false;
return nullptr;
});
if (!shmemSuccess || !copySuccess) {
const nsPrintfCString message(
(!shmemSuccess)
? "*Could not create shmem for profile from pid "
"%u (%zu B)"
: "*Could not write profile from pid %u (%zu B)",
unsigned(profiler_current_process_id().ToNumber()),
len);
if (parameters->profilerChild->AllocShmem(
message.Length() + 1, &shmem)) {
strcpy(shmem.get<char>(), message.Data());
}
}
} else {
const nsPrintfCString message(
"*Profile from pid %u bigger (%zu) than shmem max "
"(%zu)",
unsigned(profiler_current_process_id().ToNumber()), len,
size_t(UINT32_MAX));
if (parameters->profilerChild->AllocShmem(
message.Length() + 1, &shmem)) {
strcpy(shmem.get<char>(), message.Data());
}
}
writer = nullptr;
} else {
// No profile.
const char* failure =
parameters->failureLatchSource.GetFailure();
const nsPrintfCString message(
"*Could not generate profile from pid %u%s%s",
unsigned(profiler_current_process_id().ToNumber()),
failure ? ", failure: " : "", failure ? failure : "");
if (parameters->profilerChild->AllocShmem(
message.Length() + 1, &shmem)) {
strcpy(shmem.get<char>(), message.Data());
}
}
SharedLibraryInfo sharedLibraryInfo =
SharedLibraryInfo::GetInfoForSelf();
parameters->resolver(IPCProfileAndAdditionalInformation{
shmem, Some(ProfileGenerationAdditionalInformation{
std::move(sharedLibraryInfo)})});
}))))) {
// Failed to dispatch the task to the ProfilerChild thread. The IPC cannot
// be resolved on this thread, so it will never be resolved!
// And it would be unsafe to modify mGatherProfileProgress; But the parent
// should notice that's it's not advancing anymore.
}
}
mozilla::ipc::IPCResult ProfilerChild::RecvGatherProfile(
GatherProfileResolver&& aResolve) {
mGatherProfileProgress = MakeRefPtr<ProgressLogger::SharedProgress>();
mGatherProfileProgress->SetProgress(ProportionValue{0.0},
"Received gather-profile request");
auto parameters = MakeRefPtr<GatherProfileThreadParameters>(
this, mGatherProfileProgress, std::move(aResolve));
// The GatherProfileThreadFunction thread function will cast its void*
// argument to already_AddRefed<GatherProfileThreadParameters>.
parameters.get()->AddRef();
PRThread* gatherProfileThread = PR_CreateThread(
PR_SYSTEM_THREAD, GatherProfileThreadFunction, parameters.get(),
PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD, 0);
if (!gatherProfileThread) {
// Failed to create and start worker thread, resolve with an empty profile.
mozilla::ipc::Shmem shmem;
if (AllocShmem(1, &shmem)) {
shmem.get<char>()[0] = '\0';
}
parameters->resolver(IPCProfileAndAdditionalInformation{shmem, Nothing()});
// And clean up.
parameters.get()->Release();
mGatherProfileProgress = nullptr;
}
return IPC_OK();
}
mozilla::ipc::IPCResult ProfilerChild::RecvGetGatherProfileProgress(
GetGatherProfileProgressResolver&& aResolve) {
if (mGatherProfileProgress) {
aResolve(GatherProfileProgress{
mGatherProfileProgress->Progress().ToUnderlyingType(),
nsCString(mGatherProfileProgress->LastLocation())});
} else {
aResolve(
GatherProfileProgress{ProportionValue::MakeInvalid().ToUnderlyingType(),
nsCString("No gather-profile in progress")});
}
return IPC_OK();
}
void ProfilerChild::ActorDestroy(ActorDestroyReason aActorDestroyReason) {
mDestroyed = true;
}
void ProfilerChild::Destroy() {
ResetChunkManager();
if (!mDestroyed) {
Close();
}
}
ProfileAndAdditionalInformation ProfilerChild::GrabShutdownProfile() {
LOG("GrabShutdownProfile");
UniquePtr<ProfilerCodeAddressService> service =
profiler_code_address_service_for_presymbolication();
FailureLatchSource failureLatch;
SpliceableChunkedJSONWriter writer{failureLatch};
writer.Start();
auto rv = profiler_stream_json_for_this_process(
writer, /* aSinceTime */ 0,
/* aIsShuttingDown */ true, service.get(), ProgressLogger{});
if (rv.isErr()) {
const char* failure = writer.GetFailure();
return ProfileAndAdditionalInformation(
nsPrintfCString("*Profile unavailable for pid %u%s%s",
unsigned(profiler_current_process_id().ToNumber()),
failure ? ", failure: " : "", failure ? failure : ""));
}
auto additionalInfo = rv.unwrap();
writer.StartArrayProperty("processes");
writer.EndArray();
writer.End();
const size_t len = writer.ChunkedWriteFunc().Length();
// This string and information are destined to be sent as a shutdown profile,
// which is limited by the maximum IPC message size.
// JS::MaxStringLength.
if (len + additionalInfo.SizeOf() >=
size_t(IPC::Channel::kMaximumMessageSize)) {
return ProfileAndAdditionalInformation(
nsPrintfCString("*Profile from pid %u bigger (%zu) than IPC max (%zu)",
unsigned(profiler_current_process_id().ToNumber()), len,
size_t(IPC::Channel::kMaximumMessageSize)));
}
nsCString profileCString;
if (!profileCString.SetLength(len, fallible)) {
return ProfileAndAdditionalInformation(nsPrintfCString(
"*Could not allocate %zu bytes for profile from pid %u", len,
unsigned(profiler_current_process_id().ToNumber())));
}
MOZ_ASSERT(*(profileCString.Data() + len) == '\0',
"We expected a null at the end of the string buffer, to be "
"rewritten by CopyDataIntoLazilyAllocatedBuffer");
char* const profileBeginWriting = profileCString.BeginWriting();
if (!profileBeginWriting) {
return ProfileAndAdditionalInformation(
nsPrintfCString("*Could not write profile from pid %u",
unsigned(profiler_current_process_id().ToNumber())));
}
// Here, we have enough space reserved in `profileCString`, starting at
// `profileBeginWriting`, copy the JSON profile there.
if (!writer.ChunkedWriteFunc().CopyDataIntoLazilyAllocatedBuffer(
[&](size_t aBufferLen) -> char* {
MOZ_RELEASE_ASSERT(aBufferLen == len + 1);
return profileBeginWriting;
})) {
return ProfileAndAdditionalInformation(
nsPrintfCString("*Could not copy profile from pid %u",
unsigned(profiler_current_process_id().ToNumber())));
}
MOZ_ASSERT(*(profileCString.Data() + len) == '\0',
"We still expected a null at the end of the string buffer");
return ProfileAndAdditionalInformation{std::move(profileCString),
std::move(additionalInfo)};
}
} // namespace mozilla