diff --git a/deps/v8/include/v8-profiler.h b/deps/v8/include/v8-profiler.h index 927fa12e319026..a3768ecefeccc1 100644 --- a/deps/v8/include/v8-profiler.h +++ b/deps/v8/include/v8-profiler.h @@ -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; @@ -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. @@ -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 filter_context = MaybeLocal(), - CpuProfileSource profile_source = CpuProfileSource::kUnspecified); + CpuProfileSource profile_source = CpuProfileSource::kUnspecified, + SampleContextExtractor sample_context_extractor = nullptr); CpuProfilingOptions(CpuProfilingOptions&&) = default; CpuProfilingOptions& operator=(CpuProfilingOptions&&) = default; @@ -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; @@ -420,6 +502,7 @@ class V8_EXPORT CpuProfilingOptions { int sampling_interval_us_; Global filter_context_; CpuProfileSource profile_source_; + SampleContextExtractor sample_context_extractor_ = nullptr; }; /** diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index 9ef4e3b4a66006..bdde52687667ad 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -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(this); + return profile->sample(index).sample_context; +} + +void* LookupCpedMapAlignedPointer(Isolate* isolate, uintptr_t key_addr) { + i::Isolate* i_isolate = reinterpret_cast(isolate); + + i::Tagged cped_obj = + i_isolate->isolate_data()->continuation_preserved_embedder_data(); + if (!IsJSMap(cped_obj)) return nullptr; + i::Tagged map = i::Cast(cped_obj); + + i::Tagged table_obj = map->table(); + if (!IsOrderedHashMap(table_obj)) return nullptr; + i::Tagged table = + i::Cast(table_obj); + + i::Tagged key(static_cast(key_addr)); + + i::InternalIndex entry = table->FindEntry(i_isolate, key); + if (!entry.is_found()) return nullptr; + + i::Tagged value_obj = table->ValueAt(entry); + if (!IsJSObject(value_obj)) return nullptr; + i::Tagged holder = i::Cast(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(this); return profile->start_time().since_origin().InMicroseconds(); @@ -11501,15 +11537,15 @@ CpuProfiler* CpuProfiler::New(Isolate* v8_isolate, reinterpret_cast(v8_isolate), naming_mode, logging_mode)); } -CpuProfilingOptions::CpuProfilingOptions(CpuProfilingMode mode, - unsigned max_samples, - int sampling_interval_us, - MaybeLocal filter_context, - CpuProfileSource profile_source) +CpuProfilingOptions::CpuProfilingOptions( + CpuProfilingMode mode, unsigned max_samples, int sampling_interval_us, + MaybeLocal 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 local_filter_context = filter_context.ToLocalChecked(); filter_context_.Reset(v8::Isolate::GetCurrent(), local_filter_context); diff --git a/deps/v8/src/profiler/cpu-profiler.cc b/deps/v8/src/profiler/cpu-profiler.cc index 8244caf9390f47..da5f6efd0e4610 100644 --- a/deps/v8/src/profiler/cpu-profiler.cc +++ b/deps/v8/src/profiler/cpu-profiler.cc @@ -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(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_; @@ -250,7 +256,7 @@ void SamplingEventsProcessor::SymbolizeAndAddToProfiles( tick_sample.state, tick_sample.embedder_state, reinterpret_cast
(tick_sample.context), reinterpret_cast
(tick_sample.embedder_context), - tick_sample.trace_id_); + tick_sample.trace_id_, tick_sample.sample_context_); } ProfilerEventsProcessor::SampleProcessingResult @@ -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 diff --git a/deps/v8/src/profiler/cpu-profiler.h b/deps/v8/src/profiler/cpu-profiler.h index bbfc6432533e5a..a63786eee66c4e 100644 --- a/deps/v8/src/profiler/cpu-profiler.h +++ b/deps/v8/src/profiler/cpu-profiler.h @@ -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, @@ -214,6 +222,9 @@ class V8_EXPORT_PRIVATE ProfilerEventsProcessor : public base::Thread, std::atomic last_code_event_id_; unsigned last_processed_code_event_id_; Isolate* isolate_; + + private: + std::atomic sample_context_extractor_{nullptr}; }; class V8_EXPORT_PRIVATE SamplingEventsProcessor diff --git a/deps/v8/src/profiler/profile-generator.cc b/deps/v8/src/profiler/profile-generator.cc index 42f63c7707274b..bbad8ddbf6fc89 100644 --- a/deps/v8/src/profiler/profile-generator.cc +++ b/deps/v8/src/profiler/profile-generator.cc @@ -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 trace_id) { + const std::optional trace_id, + void* sample_context) { if (!CheckSubsample(sampling_interval)) return; ProfileNode* top_frame_node = top_down_.AddPathFromEnd(path, src_pos, update_stats, options_.mode()); @@ -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(profiler_->isolate())); @@ -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 trace_id) { + const std::optional 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. @@ -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); } } diff --git a/deps/v8/src/profiler/profile-generator.h b/deps/v8/src/profiler/profile-generator.h index 38b3c713de6f3b..8fa9023d83d616 100644 --- a/deps/v8/src/profiler/profile-generator.h +++ b/deps/v8/src/profiler/profile-generator.h @@ -424,6 +424,7 @@ class CpuProfile { StateTag state_tag; EmbedderStateTag embedder_state_tag; const std::optional trace_id; + void* sample_context; }; V8_EXPORT_PRIVATE CpuProfile( @@ -441,7 +442,8 @@ class CpuProfile { LineAndColumn src_pos, bool update_stats, base::TimeDelta sampling_interval, StateTag state, EmbedderStateTag embedder_state, - const std::optional trace_id = std::nullopt); + const std::optional trace_id = std::nullopt, + void* sample_context = nullptr); void FinishProfile(); const char* title() const { return title_; } @@ -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 trace_id = std::nullopt); + const std::optional trace_id = std::nullopt, + void* sample_context = nullptr); // Called from profile generator thread. void UpdateNativeContextAddressForCurrentProfiles(Address from, Address to); diff --git a/deps/v8/src/profiler/tick-sample.cc b/deps/v8/src/profiler/tick-sample.cc index af97b96d7a0f45..be5b2e561769d6 100644 --- a/deps/v8/src/profiler/tick-sample.cc +++ b/deps/v8/src/profiler/tick-sample.cc @@ -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 trace_id) { + const std::optional trace_id, + void* sample_context) { update_stats_ = update_stats; SampleInfo info; RegisterState regs = reg_state; @@ -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(); } diff --git a/deps/v8/src/profiler/tick-sample.h b/deps/v8/src/profiler/tick-sample.h index b02400777d04fd..b227bc3a655406 100644 --- a/deps/v8/src/profiler/tick-sample.h +++ b/deps/v8/src/profiler/tick-sample.h @@ -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 trace_id = std::nullopt); + const std::optional trace_id = std::nullopt, + void* sample_context = nullptr); /** * Get a call stack sample from the isolate. * \param isolate The isolate. @@ -100,6 +101,9 @@ struct V8_EXPORT TickSample { bool update_stats_ = true; // An identifier to associate the sample with a trace event. std::optional 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. }; diff --git a/doc/api/v8.md b/doc/api/v8.md index da225a333ddcac..f5a34054d6313d 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1603,9 +1603,85 @@ added: - v24.12.0 --> -* Returns: {string} +* Returns: {string|undefined} -Stopping collecting the profile and return the profile data. +Stop collecting the profile and return the profile data as a JSON-encoded +string. Calling `stop()` after the handle has already been stopped returns +`undefined`. + +### `syncCpuProfileHandle.stopAndCapture()` + + + +* Returns: {Object|undefined} + +Stop collecting the profile and return a structured profile object instead +of a JSON string. Calling `stopAndCapture()` after the handle has already +been stopped returns `undefined`. + +The returned object has shape: + +* `startTime` {number} Microseconds since V8's clock origin. +* `endTime` {number} Microseconds since V8's clock origin. +* `droppedContexts` {number} Number of samples for which a context could not + be recorded because the per-session context buffer was full. Always `0` + when the profile was started without `withContext`. +* `topDownRoot` {Object} Root of the recursive call tree: + * `functionName` {string} + * `scriptName` {string} + * `lineNumber` {number} + * `columnNumber` {number} + * `hitCount` {number} Number of samples that landed at this node. + * `contexts` {Array} Optional. Present only when at least one sample at + this node carried a context. Each element is `{ context, timestamp }`. + * `children` {Array} Child nodes (same shape). + +### `syncCpuProfileHandle.snapshot()` + + + +* Returns: {Object} + +Capture a profile of samples taken since the last `start` or `snapshot` +call, and continue sampling with a fresh internal session. Returns the +same shape as [`stopAndCapture()`][]. Useful for continuous profiling: emit +a snapshot every minute without losing samples between sessions. Throws +`ERR_INVALID_STATE` if the handle has already been stopped. + +### `syncCpuProfileHandle.runWithContext(value, fn[, ...args])` + + + +* `value` {any} Arbitrary JavaScript value to associate with samples + captured during the execution of `fn`. +* `fn` {Function} Function to invoke. +* `...args` {any} Arguments forwarded to `fn`. +* Returns: {any} The return value of `fn(...args)`. + +Run `fn(...args)` with `value` recorded as the context for any samples +captured during its synchronous execution and across awaited continuations +that propagate the context (via {AsyncLocalStorage}). Throws if the profile +was started without `withContext: true`. + +### `syncCpuProfileHandle.enterWithContext(value)` + + + +* `value` {any} Arbitrary JavaScript value to associate with subsequent + samples in the current asynchronous scope. + +Set `value` as the current sample context for the rest of the active +{AsyncLocalStorage} scope. Mirrors the naming of +[`asyncLocalStorage.enterWith()`][]. Throws if the profile was started +without `withContext: true`. ### `syncCpuProfileHandle[Symbol.dispose]()` @@ -1615,7 +1691,8 @@ added: - v24.12.0 --> -Stopping collecting the profile and the profile will be discarded. +Stops collecting the profile (equivalent to `stop()`); the profile is +discarded. ## Class: `SyncHeapProfileHandle` @@ -1783,6 +1860,19 @@ added: * `sampleInterval` {number} Requested sampling interval in milliseconds. **Default:** `0`. * `maxBufferSize` {integer} Maximum number of samples to keep before older entries are discarded. **Default:** `4294967295`. + * `withContext` {boolean} If `true`, the returned handle exposes + [`runWithContext()`][] and [`enterWithContext()`][] for associating + arbitrary JavaScript values with samples captured during their execution + scope. The values are surfaced on each sample of the structured profile + returned by [`stopAndCapture()`][] or [`snapshot()`][]. When `false` + (default), no per-sample context tracking is performed and there is no + extractor overhead. **Default:** `false`. + * `contextBufferSize` {integer} Maximum number of samples that can carry a + context value during a single profile session. Once exceeded, further + samples are recorded with no associated context and the + `droppedContexts` counter on the result is incremented. Only meaningful + when `withContext` is `true`. **Default:** `60000` (sufficient for 60 + seconds of sampling at the default 10 ms interval). * Returns: {SyncCPUProfileHandle} Starting a CPU profile then return a `SyncCPUProfileHandle` object. @@ -1794,6 +1884,21 @@ const profile = handle.stop(); console.log(profile); ``` +The returned handle can also produce a structured object tree instead of the +JSON string, and can carry a per-sample JavaScript context that propagates +across asynchronous boundaries via {AsyncLocalStorage}: + +```cjs +const handle = v8.startCpuProfile({ withContext: true }); +handle.runWithContext({ requestId: 42 }, () => { + // Synchronous and propagated-async work here is sampled with + // { requestId: 42 } associated with each sample. +}); +const profile = handle.stopAndCapture(); +// profile.topDownRoot is a recursive tree; each node may carry +// `contexts: [{ context, timestamp }, ...]`. +``` + ## `v8.startHeapProfile([options])`