Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion deps/v8/include/v8-profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,68 @@ using NativeObject = void*;
using SnapshotObjectId = uint32_t;
using ProfilerId = uint32_t;

/**
* Embedder-supplied callback invoked in signal-handler context as each
* CPU profile sample is captured. The returned pointer is stored on the
* sample and retrievable via CpuProfile::GetSampleContext.
*
* Signal-safety contract: this function is invoked from a POSIX signal
* handler (or thread-suspension context on Windows). It MUST NOT allocate
* memory, acquire locks, call any V8 API, or perform any other operation that
* is not async-signal-safe. It SHOULD limit itself to reading from memory the
* embedder keeps stable for the duration of profiling, and returning a `void*`
* whose meaning is defined by the embedder.
*
* The returned pointer is treated as opaque by V8 and is not dereferenced.
*
* The helper LookupCpedMapAlignedPointer below is provided for the common
* case in which an embedder uses a JS Map stored in
* ContinuationPreservedEmbedderData as a registry through which several
* independent libraries can attach their own continuation-bound data.
*/
using SampleContextExtractor = void* (*)(Isolate*);

/**
* Helper for SampleContextExtractor implementations that follow a common
* pattern: the embedder uses a JS Map placed in
* ContinuationPreservedEmbedderData as a shared registry, allowing multiple
* independent libraries to each store their own continuation-bound data
* under their own key in that Map without interfering with one another.
*
* This helper performs that lookup. It treats the current CPED as a JS Map,
* looks up the entry whose key has the tagged address `key_addr`, and if
* the value is a JS object with at least one internal field, returns the
* aligned pointer stored at internal field 0 (which the embedder is
* expected to have set via SetAlignedPointerInInternalField). Returns
* nullptr if CPED is not a JS Map, the key is not present, the value is
* not a JS object with an internal field, or the embedder has not stored
* an aligned pointer there.
*
* `key_addr` is the tagged address of the lookup key. The caller must
* obtain it freshly at each invocation by reading through a stable slot
* that V8 keeps GC-coherently updated — typically the persistent-handle
* slot of a v8::Global<> the embedder owns. Caching the address across
* calls would be unsafe because V8 updates the slot's contents during
* compaction. Since embedders can't necessarily reference i::Address type,
* we use uintptr_t that it typedefs. The addressed key object must have its
* hash already precomputed in order to not trigger hash computation in the
* helper. This is trivially satisfied if it was ever set as a key in a map, but
* can also be guaranteed by invoking GetIdentityHash() early on it once outside
* of signal handling.
*
* Signal-safety: performs only signal-safe operations (no allocation, no
* locks, no V8 API calls beyond raw memory reads of fixed-layout objects).
* MUST NOT be called while a V8 GC is in progress, because the helper
* walks V8 heap state (CPED, JSMap, OrderedHashMap, JSObject internal
* fields) which may be mid-compaction. Embedders should install
* Isolate::AddGCPrologueCallback / AddGCEpilogueCallback to observe GC and
* refrain from invoking this helper while in GC. It is safe though to capture
* this helper's return value once at the prologue (a safe point on the JS
* thread) and serve it from a cache while GC is in progress.
*/
V8_EXPORT void* LookupCpedMapAlignedPointer(Isolate* isolate,
uintptr_t key_addr);

struct CpuProfileDeoptFrame {
int script_id;
size_t position;
Expand Down Expand Up @@ -272,6 +334,15 @@ class V8_EXPORT CpuProfile {
*/
EmbedderStateTag GetSampleEmbedderState(int index) const;

/**
* Returns the embedder-supplied sample context for the sample at the given
* index. The pointer was produced by the SampleContextExtractor installed on
* the CpuProfilingOptions used to start this profile. If no extractor was
* installed, or the extractor returned nullptr for this sample, returns
* nullptr.
*/
void* GetSampleContext(int index) const;

/**
* Returns time when the profile recording was stopped (in microseconds)
* since some unspecified starting point.
Expand Down Expand Up @@ -394,12 +465,20 @@ class V8_EXPORT CpuProfilingOptions {
* \param filter_context If specified, profiles will only contain frames
* using this context. Other frames will be elided.
* \param profile_source Identifies the source of this CPU profile.
* \param sample_context_extractor Optional embedder callback invoked in
* signal-handler context as each sample is
* captured. The returned pointer is stored
* on the sample and retrievable via
* CpuProfile::GetSampleContext. See
* SampleContextExtractor for the
* signal-safety contract.
*/
CpuProfilingOptions(
CpuProfilingMode mode = kLeafNodeLineNumbers,
unsigned max_samples = kNoSampleLimit, int sampling_interval_us = 0,
MaybeLocal<Context> filter_context = MaybeLocal<Context>(),
CpuProfileSource profile_source = CpuProfileSource::kUnspecified);
CpuProfileSource profile_source = CpuProfileSource::kUnspecified,
SampleContextExtractor sample_context_extractor = nullptr);

CpuProfilingOptions(CpuProfilingOptions&&) = default;
CpuProfilingOptions& operator=(CpuProfilingOptions&&) = default;
Expand All @@ -408,6 +487,9 @@ class V8_EXPORT CpuProfilingOptions {
unsigned max_samples() const { return max_samples_; }
int sampling_interval_us() const { return sampling_interval_us_; }
CpuProfileSource profile_source() const { return profile_source_; }
SampleContextExtractor sample_context_extractor() const {
return sample_context_extractor_;
}

private:
friend class internal::CpuProfile;
Expand All @@ -420,6 +502,7 @@ class V8_EXPORT CpuProfilingOptions {
int sampling_interval_us_;
Global<Context> filter_context_;
CpuProfileSource profile_source_;
SampleContextExtractor sample_context_extractor_ = nullptr;
};

/**
Expand Down
48 changes: 42 additions & 6 deletions deps/v8/src/api/api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11465,6 +11465,42 @@ EmbedderStateTag CpuProfile::GetSampleEmbedderState(int index) const {
return profile->sample(index).embedder_state_tag;
}

void* CpuProfile::GetSampleContext(int index) const {
const i::CpuProfile* profile = reinterpret_cast<const i::CpuProfile*>(this);
return profile->sample(index).sample_context;
}

void* LookupCpedMapAlignedPointer(Isolate* isolate, uintptr_t key_addr) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);

i::Tagged<i::Object> cped_obj =
i_isolate->isolate_data()->continuation_preserved_embedder_data();
if (!IsJSMap(cped_obj)) return nullptr;
i::Tagged<i::JSMap> map = i::Cast<i::JSMap>(cped_obj);

i::Tagged<i::Object> table_obj = map->table();
if (!IsOrderedHashMap(table_obj)) return nullptr;
i::Tagged<i::OrderedHashMap> table =
i::Cast<i::OrderedHashMap>(table_obj);

i::Tagged<i::Object> key(static_cast<i::Address>(key_addr));

i::InternalIndex entry = table->FindEntry(i_isolate, key);
if (!entry.is_found()) return nullptr;

i::Tagged<i::Object> value_obj = table->ValueAt(entry);
if (!IsJSObject(value_obj)) return nullptr;
i::Tagged<i::JSObject> holder = i::Cast<i::JSObject>(value_obj);

void* aligned_ptr = nullptr;
if (!i::EmbedderDataSlot(holder, 0).ToAlignedPointer(
i_isolate, &aligned_ptr,
{i::kFirstEmbedderDataTag, i::kLastEmbedderDataTag})) {
return nullptr;
}
return aligned_ptr;
}

int64_t CpuProfile::GetStartTime() const {
const i::CpuProfile* profile = reinterpret_cast<const i::CpuProfile*>(this);
return profile->start_time().since_origin().InMicroseconds();
Expand Down Expand Up @@ -11501,15 +11537,15 @@ CpuProfiler* CpuProfiler::New(Isolate* v8_isolate,
reinterpret_cast<i::Isolate*>(v8_isolate), naming_mode, logging_mode));
}

CpuProfilingOptions::CpuProfilingOptions(CpuProfilingMode mode,
unsigned max_samples,
int sampling_interval_us,
MaybeLocal<Context> filter_context,
CpuProfileSource profile_source)
CpuProfilingOptions::CpuProfilingOptions(
CpuProfilingMode mode, unsigned max_samples, int sampling_interval_us,
MaybeLocal<Context> filter_context, CpuProfileSource profile_source,
SampleContextExtractor sample_context_extractor)
: mode_(mode),
max_samples_(max_samples),
sampling_interval_us_(sampling_interval_us),
profile_source_(profile_source) {
profile_source_(profile_source),
sample_context_extractor_(sample_context_extractor) {
if (!filter_context.IsEmpty()) {
Local<Context> local_filter_context = filter_context.ToLocalChecked();
filter_context_.Reset(v8::Isolate::GetCurrent(), local_filter_context);
Expand Down
14 changes: 12 additions & 2 deletions deps/v8/src/profiler/cpu-profiler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@ class CpuSampler : public sampler::Sampler {
}
// Every bailout up until here resulted in a dropped sample. From now on,
// the sample is created in the buffer.

void* sample_context = nullptr;
if (auto extractor = processor_->sample_context_extractor()) {
sample_context = extractor(reinterpret_cast<v8::Isolate*>(isolate));
}
sample->Init(isolate, regs, TickSample::kIncludeCEntryFrame,
/* update_stats */ true,
/* use_simulator_reg_state */ true, processor_->period());
/* use_simulator_reg_state */ true, processor_->period(),
/* trace_id */ std::nullopt, sample_context);
if (is_counting_samples_ && !sample->timestamp.IsNull()) {
if (sample->state == JS) ++js_sample_count_;
if (sample->state == EXTERNAL) ++external_sample_count_;
Expand Down Expand Up @@ -250,7 +256,7 @@ void SamplingEventsProcessor::SymbolizeAndAddToProfiles(
tick_sample.state, tick_sample.embedder_state,
reinterpret_cast<Address>(tick_sample.context),
reinterpret_cast<Address>(tick_sample.embedder_context),
tick_sample.trace_id_);
tick_sample.trace_id_, tick_sample.sample_context_);
}

ProfilerEventsProcessor::SampleProcessingResult
Expand Down Expand Up @@ -656,6 +662,10 @@ CpuProfilingResult CpuProfiler::StartProfiling(
TRACE_EVENT0("v8", "CpuProfiler::StartProfiling");
AdjustSamplingInterval();
StartProcessorIfNotStarted();
auto sample_context_extractor = options.sample_context_extractor();
if (sample_context_extractor != nullptr) {
processor_->set_sample_context_extractor(sample_context_extractor);
}

// Collect script rundown at the start of profiling if trace category is
// turned on
Expand Down
11 changes: 11 additions & 0 deletions deps/v8/src/profiler/cpu-profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ class V8_EXPORT_PRIVATE ProfilerEventsProcessor : public base::Thread,

virtual void SetSamplingInterval(base::TimeDelta) {}

using SampleContextExtractor = void* (*)(v8::Isolate*);
void set_sample_context_extractor(SampleContextExtractor fn) {
sample_context_extractor_.store(fn, std::memory_order_release);
}
SampleContextExtractor sample_context_extractor() const {
return sample_context_extractor_.load(std::memory_order_acquire);
}

protected:
ProfilerEventsProcessor(Isolate* isolate, Symbolizer* symbolizer,
ProfilerCodeObserver* code_observer,
Expand All @@ -214,6 +222,9 @@ class V8_EXPORT_PRIVATE ProfilerEventsProcessor : public base::Thread,
std::atomic<unsigned> last_code_event_id_;
unsigned last_processed_code_event_id_;
Isolate* isolate_;

private:
std::atomic<SampleContextExtractor> sample_context_extractor_{nullptr};
};

class V8_EXPORT_PRIVATE SamplingEventsProcessor
Expand Down
9 changes: 5 additions & 4 deletions deps/v8/src/profiler/profile-generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,8 @@ void CpuProfile::AddPath(base::TimeTicks timestamp,
bool update_stats, base::TimeDelta sampling_interval,
StateTag state_tag,
EmbedderStateTag embedder_state_tag,
const std::optional<uint64_t> trace_id) {
const std::optional<uint64_t> trace_id,
void* sample_context) {
if (!CheckSubsample(sampling_interval)) return;
ProfileNode* top_frame_node =
top_down_.AddPathFromEnd(path, src_pos, update_stats, options_.mode());
Expand All @@ -659,7 +660,7 @@ void CpuProfile::AddPath(base::TimeTicks timestamp,

if (should_record_sample) {
samples_.push_back({top_frame_node, timestamp, src_pos, state_tag,
embedder_state_tag, trace_id});
embedder_state_tag, trace_id, sample_context});
} else if (is_buffer_full && delegate_ != nullptr) {
const auto task_runner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(
reinterpret_cast<v8::Isolate*>(profiler_->isolate()));
Expand Down Expand Up @@ -1230,7 +1231,7 @@ void CpuProfilesCollection::AddPathToCurrentProfiles(
LineAndColumn src_pos, bool update_stats, base::TimeDelta sampling_interval,
StateTag state, EmbedderStateTag embedder_state_tag,
Address native_context_address, Address embedder_native_context_address,
const std::optional<uint64_t> trace_id) {
const std::optional<uint64_t> trace_id, void* sample_context) {
// As starting / stopping profiles is rare relatively to this
// method, we don't bother minimizing the duration of lock holding,
// e.g. copying contents of the list to a local vector.
Expand All @@ -1254,7 +1255,7 @@ void CpuProfilesCollection::AddPathToCurrentProfiles(
timestamp, accepts_context ? path : empty_path, src_pos, update_stats,
sampling_interval, state,
accepts_embedder_context ? embedder_state_tag : EmbedderStateTag::EMPTY,
trace_id);
trace_id, sample_context);
}
}

Expand Down
7 changes: 5 additions & 2 deletions deps/v8/src/profiler/profile-generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ class CpuProfile {
StateTag state_tag;
EmbedderStateTag embedder_state_tag;
const std::optional<uint64_t> trace_id;
void* sample_context;
};

V8_EXPORT_PRIVATE CpuProfile(
Expand All @@ -441,7 +442,8 @@ class CpuProfile {
LineAndColumn src_pos, bool update_stats,
base::TimeDelta sampling_interval, StateTag state,
EmbedderStateTag embedder_state,
const std::optional<uint64_t> trace_id = std::nullopt);
const std::optional<uint64_t> trace_id = std::nullopt,
void* sample_context = nullptr);
void FinishProfile();

const char* title() const { return title_; }
Expand Down Expand Up @@ -588,7 +590,8 @@ class V8_EXPORT_PRIVATE CpuProfilesCollection {
EmbedderStateTag embedder_state_tag,
Address native_context_address = kNullAddress,
Address native_embedder_context_address = kNullAddress,
const std::optional<uint64_t> trace_id = std::nullopt);
const std::optional<uint64_t> trace_id = std::nullopt,
void* sample_context = nullptr);

// Called from profile generator thread.
void UpdateNativeContextAddressForCurrentProfiles(Address from, Address to);
Expand Down
4 changes: 3 additions & 1 deletion deps/v8/src/profiler/tick-sample.cc
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ DISABLE_ASAN void TickSample::Init(Isolate* v8_isolate,
bool update_stats,
bool use_simulator_reg_state,
base::TimeDelta sampling_interval,
const std::optional<uint64_t> trace_id) {
const std::optional<uint64_t> trace_id,
void* sample_context) {
update_stats_ = update_stats;
SampleInfo info;
RegisterState regs = reg_state;
Expand Down Expand Up @@ -209,6 +210,7 @@ DISABLE_ASAN void TickSample::Init(Isolate* v8_isolate,
}
sampling_interval_ = sampling_interval;
trace_id_ = trace_id;
sample_context_ = sample_context;
timestamp = base::TimeTicks::Now();
}

Expand Down
6 changes: 5 additions & 1 deletion deps/v8/src/profiler/tick-sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ struct V8_EXPORT TickSample {
RecordCEntryFrame record_c_entry_frame, bool update_stats,
bool use_simulator_reg_state = true,
base::TimeDelta sampling_interval = base::TimeDelta(),
const std::optional<uint64_t> trace_id = std::nullopt);
const std::optional<uint64_t> trace_id = std::nullopt,
void* sample_context = nullptr);
/**
* Get a call stack sample from the isolate.
* \param isolate The isolate.
Expand Down Expand Up @@ -100,6 +101,9 @@ struct V8_EXPORT TickSample {
bool update_stats_ = true;
// An identifier to associate the sample with a trace event.
std::optional<uint64_t> trace_id_;
// Embedder-supplied opaque value captured by SampleContextExtractor. See
// v8::SampleContextExtractor and v8::CpuProfile::GetSampleContext.
void* sample_context_ = nullptr;

void* stack[kMaxFramesCount]; // Call stack.
};
Expand Down
Loading