commit 4157a13ac228a5fcd0490475edb35c71c835c327
parent 8cfac1e837d98af7dd1ea4f3c7a7379243d0834f
Author: Nazım Can Altınova <canaltinova@gmail.com>
Date: Tue, 21 Oct 2025 21:15:34 +0000
Bug 1979114 - Add a JS API to get JS script sources r=iain,arai
This patch adds a new JS API for fetching the JS source data of the
JS frames that was captured during that profiling run.
We have a few constraints from the profiler:
1. We have to be able to call it from a non-main thread. So we shouldn't
rely on any MainThread only data.
2. We shouldn't rely on a realm. We should directly return the source
string if available.
3. If the string is not available and if it's not possible to fetch it
in a non-main thread, we should return the necessary information for
that source, so the profiler can request it later, inside the parent
process main thread.
Because of these constrains, lots of things had to be reimplemented to
fit inside these constraints.
`ScriptSource::loadSource`, `ScriptSource::substring`, and
`ScriptSource::functionBodyString` requires them be run on the main
thread, that's why we couldn't use them. I implemented
`getSourceProperties`, `substringChars`, and `functionBodyStringChars`
respectively that can be run outside of the main thread.
Also I crated a struct to pass this to the profiler codebase. This will
be used to send the everything to the parent process.
For `Retrievable<Unit>` cases, we don't have the source code available
for them in this process. We can request them, but it requires calling
sourceHook, which also needs to be run on the main thread. It also sends
an IPC to the parent processs to request the data. Since we are
returning these sources to the parent process, we can delay fetching
them until we are on there. So we record all the necessary information
(filename) to fetch this data later, and sending that to the parent
process.
Differential Revision: https://phabricator.services.mozilla.com/D259263
Diffstat:
7 files changed, 488 insertions(+), 39 deletions(-)
diff --git a/js/public/ProfilingSources.h b/js/public/ProfilingSources.h
@@ -0,0 +1,166 @@
+/* -*- 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef js_ProfilingSources_h
+#define js_ProfilingSources_h
+
+#include "mozilla/Variant.h"
+
+#include <stdint.h>
+
+#include "jstypes.h"
+
+#include "js/TypeDecls.h"
+#include "js/Utility.h"
+#include "js/Vector.h"
+
+/*
+ * Struct to pass JS source data with content type information for profiler use.
+ */
+struct JS_PUBLIC_API ProfilerJSSourceData {
+ public:
+ struct SourceTextUTF16 {
+ public:
+ SourceTextUTF16(JS::UniqueTwoByteChars&& c, size_t l)
+ : chars_(std::move(c)), length_(l) {}
+
+ const JS::UniqueTwoByteChars& chars() const { return chars_; }
+ size_t length() const { return length_; }
+
+ private:
+ // Not null-terminated string for source text. Always use it with the
+ // length.
+ JS::UniqueTwoByteChars chars_;
+ size_t length_;
+ };
+
+ struct SourceTextUTF8 {
+ public:
+ SourceTextUTF8(JS::UniqueChars&& c, size_t l)
+ : chars_(std::move(c)), length_(l) {}
+
+ const JS::UniqueChars& chars() const { return chars_; }
+ size_t length() const { return length_; }
+
+ private:
+ // Not null-terminated string for source text. Always use it with the
+ // length.
+ JS::UniqueChars chars_;
+ size_t length_;
+ };
+
+ /*
+ * Represents a source file that can be retrieved later in the parent process.
+ * Used when source text is not immediately available in the current process
+ * but can be fetched using the file path information.
+ */
+ struct RetrievableFile {};
+
+ struct Unavailable {};
+
+ using ProfilerSourceVariant =
+ mozilla::Variant<SourceTextUTF16, SourceTextUTF8, RetrievableFile,
+ Unavailable>;
+
+ // Constructors
+ ProfilerJSSourceData(uint32_t sourceId, JS::UniqueChars&& filePath,
+ size_t pathLen)
+ : sourceId_(sourceId),
+ filePath_(std::move(filePath)),
+ filePathLength_(pathLen),
+ data_(Unavailable{}) {}
+
+ // UTF-8 source text with filePath
+ ProfilerJSSourceData(uint32_t sourceId, JS::UniqueChars&& chars,
+ size_t length, JS::UniqueChars&& filePath,
+ size_t pathLen)
+ : sourceId_(sourceId),
+ filePath_(std::move(filePath)),
+ filePathLength_(pathLen),
+ data_(SourceTextUTF8{std::move(chars), length}) {}
+
+ // UTF-16 source text with filePath
+ ProfilerJSSourceData(uint32_t sourceId, JS::UniqueTwoByteChars&& chars,
+ size_t length, JS::UniqueChars&& filePath,
+ size_t pathLen)
+ : sourceId_(sourceId),
+ filePath_(std::move(filePath)),
+ filePathLength_(pathLen),
+ data_(SourceTextUTF16{std::move(chars), length}) {}
+
+ ProfilerJSSourceData()
+ : sourceId_(0), filePathLength_(0), data_(Unavailable{}) {}
+
+ static ProfilerJSSourceData CreateRetrievableFile(uint32_t sourceId,
+ JS::UniqueChars&& filePath,
+ size_t pathLength) {
+ ProfilerJSSourceData result(sourceId, std::move(filePath), pathLength);
+ result.data_.emplace<RetrievableFile>();
+ return result;
+ }
+
+ ProfilerJSSourceData(ProfilerJSSourceData&&) = default;
+ ProfilerJSSourceData& operator=(ProfilerJSSourceData&&) = default;
+
+ // No copy constructors as this class owns its string storage.
+ ProfilerJSSourceData(const ProfilerJSSourceData& other) = delete;
+ ProfilerJSSourceData& operator=(const ProfilerJSSourceData&) = delete;
+
+ uint32_t sourceId() const { return sourceId_; }
+ // Consumer should always check for filePathLength before calling this.
+ const char* filePath() const {
+ MOZ_ASSERT(filePath_);
+ return filePath_.get();
+ }
+ size_t filePathLength() const { return filePathLength_; }
+ const ProfilerSourceVariant& data() const { return data_; }
+
+ // Used only for memory reporting.
+ size_t SizeOf() const {
+ // Size of sourceId + filepath
+ size_t size = sizeof(uint32_t) + filePathLength_ * sizeof(char);
+
+ data_.match(
+ [&](const SourceTextUTF16& srcText) {
+ size += srcText.length() * sizeof(char16_t);
+ },
+ [&](const SourceTextUTF8& srcText) {
+ size += srcText.length() * sizeof(char);
+ },
+ [](const RetrievableFile&) {}, [](const Unavailable&) {});
+
+ return size;
+ }
+
+ private:
+ // Unique identifier for this source across the process. This can be used
+ // to refer to this source from places that don't want to hold a strong
+ // reference on the source itself.
+ // Generated by ScriptSource and retrieved via ScriptSource::id(). See
+ // ScriptSource::id_ for more details.
+ uint32_t sourceId_;
+ // Null-terminated file path for the source.
+ // It can be nullptr if:
+ // - The source has no filename.
+ // - filename allocation fails during copy.
+ JS::UniqueChars filePath_;
+ size_t filePathLength_;
+ ProfilerSourceVariant data_;
+};
+
+namespace js {
+
+using ProfilerJSSources =
+ js::Vector<ProfilerJSSourceData, 0, js::SystemAllocPolicy>;
+
+/*
+ * Main API for getting the profiled JS sources.
+ */
+JS_PUBLIC_API ProfilerJSSources GetProfilerScriptSources(JSRuntime* rt);
+
+} // namespace js
+
+#endif /* js_ProfilingSources_h */
diff --git a/js/src/debugger/Source.cpp b/js/src/debugger/Source.cpp
@@ -217,16 +217,7 @@ class DebuggerSourceGetTextMatcher {
return NewStringCopyZ<CanGC>(cx_, "[no source]");
}
- // In case of DOM event handler like <div onclick="foo()" the JS code is
- // wrapped into
- // function onclick() {foo()}
- // We want to only return `foo()` here.
- // But only for event handlers, for `new Function("foo()")`, we want to
- // return:
- // function anonymous() {foo()}
- if (ss->hasIntroductionType() &&
- strcmp(ss->introductionType(), "eventHandler") == 0 &&
- ss->isFunctionBody()) {
+ if (ss->shouldUnwrapEventHandlerBody()) {
return ss->functionBodyString(cx_);
}
diff --git a/js/src/moz.build b/js/src/moz.build
@@ -180,6 +180,7 @@ EXPORTS.js += [
"../public/Printf.h",
"../public/ProfilingCategory.h",
"../public/ProfilingFrameIterator.h",
+ "../public/ProfilingSources.h",
"../public/ProfilingStack.h",
"../public/Promise.h",
"../public/PropertyAndElement.h",
diff --git a/js/src/vm/GeckoProfiler.cpp b/js/src/vm/GeckoProfiler.cpp
@@ -26,6 +26,7 @@
#include "jit/JSJitFrameIter-inl.h"
using namespace js;
+using mozilla::Utf8Unit;
GeckoProfilerThread::GeckoProfilerThread()
: profilingStack_(nullptr), profilingStackIfEnabled_(nullptr) {}
@@ -446,6 +447,96 @@ void GeckoProfilerRuntime::checkStringsMapAfterMovingGC() {
}
#endif
+// Get all script sources as a list of ProfilerJSSourceData.
+js::ProfilerJSSources GeckoProfilerRuntime::getProfilerScriptSources() {
+ js::ProfilerJSSources result;
+
+ auto guard = scriptSources_.readLock();
+ for (auto iter = guard->iter(); !iter.done(); iter.next()) {
+ const RefPtr<ScriptSource>& scriptSource = iter.get();
+ MOZ_ASSERT(scriptSource);
+
+ bool hasSourceText;
+ bool retrievableSource;
+ ScriptSource::getSourceProperties(scriptSource, &hasSourceText,
+ &retrievableSource);
+
+ uint32_t sourceId = scriptSource->id();
+
+ // Get filename for all source types. Create single copy to be moved.
+ const char* filename = scriptSource->filename();
+ size_t filenameLen = 0;
+ JS::UniqueChars filenameCopy;
+ if (filename) {
+ filenameLen = strlen(filename);
+ filenameCopy.reset(static_cast<char*>(js_malloc(filenameLen + 1)));
+ if (filenameCopy) {
+ strcpy(filenameCopy.get(), filename);
+ }
+ }
+
+ if (retrievableSource) {
+ (void)result.append(ProfilerJSSourceData::CreateRetrievableFile(
+ sourceId, std::move(filenameCopy), filenameLen));
+ continue;
+ }
+
+ if (!hasSourceText) {
+ (void)result.append(
+ ProfilerJSSourceData(sourceId, std::move(filenameCopy), filenameLen));
+ continue;
+ }
+
+ size_t sourceLength = scriptSource->length();
+ if (sourceLength == 0) {
+ (void)result.append(
+ ProfilerJSSourceData(sourceId, JS::UniqueTwoByteChars(), 0,
+ std::move(filenameCopy), filenameLen));
+ continue;
+ }
+
+ SubstringCharsResult sourceResult(JS::UniqueChars(nullptr));
+ size_t charsLength = 0;
+
+ if (scriptSource->shouldUnwrapEventHandlerBody()) {
+ sourceResult = scriptSource->functionBodyStringChars(&charsLength);
+
+ if (charsLength == 0) {
+ (void)result.append(
+ ProfilerJSSourceData(sourceId, JS::UniqueTwoByteChars(), 0,
+ std::move(filenameCopy), filenameLen));
+ continue;
+ }
+ } else {
+ sourceResult = scriptSource->substringChars(0, sourceLength);
+ charsLength = sourceLength;
+ }
+
+ // Convert SubstringCharsResult to ProfilerJSSourceData.
+ // Note: The returned buffers are NOT null-terminated. The length is
+ // tracked separately in charsLength and passed to ProfilerJSSourceData.
+ if (sourceResult.is<JS::UniqueChars>()) {
+ auto& utf8Chars = sourceResult.as<JS::UniqueChars>();
+ if (!utf8Chars) {
+ continue;
+ }
+ (void)result.append(
+ ProfilerJSSourceData(sourceId, std::move(utf8Chars), charsLength,
+ std::move(filenameCopy), filenameLen));
+ } else {
+ auto& utf16Chars = sourceResult.as<JS::UniqueTwoByteChars>();
+ if (!utf16Chars) {
+ continue;
+ }
+ (void)result.append(
+ ProfilerJSSourceData(sourceId, std::move(utf16Chars), charsLength,
+ std::move(filenameCopy), filenameLen));
+ }
+ }
+
+ return result;
+}
+
void ProfilingStackFrame::trace(JSTracer* trc) {
if (isJsFrame()) {
JSScript* s = rawScript();
@@ -569,6 +660,11 @@ JS_PUBLIC_API void js::RegisterContextProfilerMarkers(
terminatingFlowMarker);
}
+JS_PUBLIC_API js::ProfilerJSSources js::GetProfilerScriptSources(
+ JSRuntime* rt) {
+ return rt->geckoProfiler().getProfilerScriptSources();
+}
+
AutoSuppressProfilerSampling::AutoSuppressProfilerSampling(JSContext* cx)
: cx_(cx), previouslyEnabled_(cx->isProfilerSamplingEnabled()) {
if (previouslyEnabled_) {
diff --git a/js/src/vm/GeckoProfiler.h b/js/src/vm/GeckoProfiler.h
@@ -188,6 +188,7 @@ class GeckoProfilerRuntime {
void stringsReset();
bool insertScriptSource(ScriptSource* scriptSource) {
+ MOZ_ASSERT(scriptSource);
auto guard = scriptSources_.writeLock();
if (!enabled_) {
return true;
@@ -196,6 +197,8 @@ class GeckoProfilerRuntime {
return guard->put(scriptSource);
}
+ js::ProfilerJSSources getProfilerScriptSources();
+
const uint32_t* addressOfEnabled() const { return &enabled_; }
void fixupStringsMapAfterMovingGC();
diff --git a/js/src/vm/JSScript.cpp b/js/src/vm/JSScript.cpp
@@ -834,6 +834,7 @@ void ScriptSourceObject::clearPrivate(JSRuntime* rt) {
getSlotRef(PRIVATE_SLOT).setUndefinedUnchecked();
}
+// Main-thread source loader that can retrieve sources via the source hook.
class ScriptSource::LoadSourceMatcher {
JSContext* const cx_;
ScriptSource* const ss_;
@@ -855,6 +856,11 @@ class ScriptSource::LoadSourceMatcher {
return true;
}
+ bool operator()(const Missing&) const {
+ *loaded_ = false;
+ return true;
+ }
+
template <typename Unit>
bool operator()(const Retrievable<Unit>&) {
if (!cx_->runtime()->sourceHook.ref()) {
@@ -872,11 +878,6 @@ class ScriptSource::LoadSourceMatcher {
return true;
}
- bool operator()(const Missing&) const {
- *loaded_ = false;
- return true;
- }
-
private:
bool tryLoadAndSetSource(const Utf8Unit&, size_t* length) const {
char* utf8Source;
@@ -927,6 +928,46 @@ bool ScriptSource::loadSource(JSContext* cx, ScriptSource* ss, bool* loaded) {
return ss->data.match(LoadSourceMatcher(cx, ss, loaded));
}
+// Matcher to get source properties: whether source is present and whether
+// it is retrievable.
+class ScriptSource::SourcePropertiesGetter {
+ bool* const hasSourceText_;
+ bool* const retrievable_;
+
+ public:
+ explicit SourcePropertiesGetter(bool* hasSourceText, bool* retrievable)
+ : hasSourceText_(hasSourceText), retrievable_(retrievable) {}
+
+ template <typename Unit, SourceRetrievable CanRetrieve>
+ void operator()(const Compressed<Unit, CanRetrieve>&) const {
+ *hasSourceText_ = true;
+ *retrievable_ = false;
+ }
+
+ template <typename Unit, SourceRetrievable CanRetrieve>
+ void operator()(const Uncompressed<Unit, CanRetrieve>&) const {
+ *hasSourceText_ = true;
+ *retrievable_ = false;
+ }
+
+ template <typename Unit>
+ void operator()(const Retrievable<Unit>&) {
+ // Retrievable requires the main thread. Do not attempt to retrieve it.
+ *hasSourceText_ = false;
+ *retrievable_ = true;
+ }
+
+ void operator()(const Missing&) const {
+ *hasSourceText_ = false;
+ *retrievable_ = false;
+ }
+};
+
+void ScriptSource::getSourceProperties(ScriptSource* ss, bool* hasSourceText,
+ bool* retrievable) {
+ ss->data.match(SourcePropertiesGetter(hasSourceText, retrievable));
+}
+
/* static */
JSLinearString* JSScript::sourceData(JSContext* cx, HandleScript script) {
MOZ_ASSERT(script->scriptSource()->hasSourceText());
@@ -1019,14 +1060,18 @@ size_t UncompressedSourceCache::sizeOfExcludingThis(
template <typename Unit>
const Unit* ScriptSource::chunkUnits(
- JSContext* cx, UncompressedSourceCache::AutoHoldEntry& holder,
+ JSContext* maybeCx, UncompressedSourceCache::AutoHoldEntry& holder,
size_t chunk) {
const CompressedData<Unit>& c = *compressedData<Unit>();
- ScriptSourceChunk ssc(this, chunk);
- if (const Unit* decompressed =
- cx->caches().uncompressedSourceCache.lookup<Unit>(ssc, holder)) {
- return decompressed;
+ // Try cache lookup only if we have a JSContext
+ if (maybeCx) {
+ ScriptSourceChunk ssc(this, chunk);
+ if (const Unit* decompressed =
+ maybeCx->caches().uncompressedSourceCache.lookup<Unit>(ssc,
+ holder)) {
+ return decompressed;
+ }
}
size_t totalLengthInBytes = length() * sizeof(Unit);
@@ -1036,7 +1081,9 @@ const Unit* ScriptSource::chunkUnits(
const size_t chunkLength = chunkBytes / sizeof(Unit);
EntryUnits<Unit> decompressed(js_pod_malloc<Unit>(chunkLength));
if (!decompressed) {
- JS_ReportOutOfMemory(cx);
+ if (maybeCx) {
+ JS_ReportOutOfMemory(maybeCx);
+ }
return nullptr;
}
@@ -1045,16 +1092,27 @@ const Unit* ScriptSource::chunkUnits(
if (!DecompressStringChunk(
reinterpret_cast<const unsigned char*>(c.raw.chars()), chunk,
reinterpret_cast<unsigned char*>(decompressed.get()), chunkBytes)) {
- JS_ReportOutOfMemory(cx);
+ if (maybeCx) {
+ JS_ReportOutOfMemory(maybeCx);
+ }
return nullptr;
}
const Unit* ret = decompressed.get();
- if (!cx->caches().uncompressedSourceCache.put(
- ssc, ToSourceData(std::move(decompressed)), holder)) {
- JS_ReportOutOfMemory(cx);
- return nullptr;
+
+ // Try to cache the result only if we have a JSContext
+ if (maybeCx) {
+ ScriptSourceChunk ssc(this, chunk);
+ if (!maybeCx->caches().uncompressedSourceCache.put(
+ ssc, ToSourceData(std::move(decompressed)), holder)) {
+ JS_ReportOutOfMemory(maybeCx);
+ return nullptr;
+ }
+ } else {
+ // Without caching, transfer ownership to holder for memory management
+ holder.holdUnits(std::move(decompressed));
}
+
return ret;
}
@@ -1127,7 +1185,7 @@ ScriptSource::PinnedUnitsIfUncompressed<Unit>::~PinnedUnitsIfUncompressed() {
}
template <typename Unit>
-const Unit* ScriptSource::units(JSContext* cx,
+const Unit* ScriptSource::units(JSContext* maybeCx,
UncompressedSourceCache::AutoHoldEntry& holder,
size_t begin, size_t len) {
MOZ_ASSERT(begin <= length());
@@ -1170,7 +1228,7 @@ const Unit* ScriptSource::units(JSContext* cx,
// Directly return units within a single chunk. UncompressedSourceCache
// and |holder| will hold the units alive past function return.
if (firstChunk == lastChunk) {
- const Unit* units = chunkUnits<Unit>(cx, holder, firstChunk);
+ const Unit* units = chunkUnits<Unit>(maybeCx, holder, firstChunk);
if (!units) {
return nullptr;
}
@@ -1182,7 +1240,9 @@ const Unit* ScriptSource::units(JSContext* cx,
// decompressed units into freshly-allocated memory to return.
EntryUnits<Unit> decompressed(js_pod_malloc<Unit>(len));
if (!decompressed) {
- JS_ReportOutOfMemory(cx);
+ if (maybeCx) {
+ JS_ReportOutOfMemory(maybeCx);
+ }
return nullptr;
}
@@ -1195,7 +1255,7 @@ const Unit* ScriptSource::units(JSContext* cx,
// with multiple chunks, and we must use and destroy distinct, fresh
// holders for each chunk.
UncompressedSourceCache::AutoHoldEntry firstHolder;
- const Unit* units = chunkUnits<Unit>(cx, firstHolder, firstChunk);
+ const Unit* units = chunkUnits<Unit>(maybeCx, firstHolder, firstChunk);
if (!units) {
return nullptr;
}
@@ -1206,7 +1266,7 @@ const Unit* ScriptSource::units(JSContext* cx,
for (size_t i = firstChunk + 1; i < lastChunk; i++) {
UncompressedSourceCache::AutoHoldEntry chunkHolder;
- const Unit* units = chunkUnits<Unit>(cx, chunkHolder, i);
+ const Unit* units = chunkUnits<Unit>(maybeCx, chunkHolder, i);
if (!units) {
return nullptr;
}
@@ -1216,7 +1276,7 @@ const Unit* ScriptSource::units(JSContext* cx,
{
UncompressedSourceCache::AutoHoldEntry lastHolder;
- const Unit* units = chunkUnits<Unit>(cx, lastHolder, lastChunk);
+ const Unit* units = chunkUnits<Unit>(maybeCx, lastHolder, lastChunk);
if (!units) {
return nullptr;
}
@@ -1250,14 +1310,14 @@ const Unit* ScriptSource::uncompressedUnits(size_t begin, size_t len) {
template <typename Unit>
ScriptSource::PinnedUnits<Unit>::PinnedUnits(
- JSContext* cx, ScriptSource* source,
+ JSContext* maybeCx, ScriptSource* source,
UncompressedSourceCache::AutoHoldEntry& holder, size_t begin, size_t len)
: PinnedUnitsBase(source) {
MOZ_ASSERT(source->hasSourceType<Unit>(), "must pin units of source's type");
addReader();
- units_ = source->units<Unit>(cx, holder, begin, len);
+ units_ = source->units<Unit>(maybeCx, holder, begin, len);
if (!units_) {
removeReader<Unit>();
}
@@ -1347,6 +1407,62 @@ JSLinearString* ScriptSource::substringDontDeflate(JSContext* cx, size_t start,
return NewStringCopyNDontDeflate<CanGC>(cx, units.asChars(), len);
}
+SubstringCharsResult ScriptSource::substringChars(size_t start, size_t stop) {
+ MOZ_ASSERT(start <= stop);
+
+ size_t len = stop - start;
+ MOZ_ASSERT(len > 0, "Callers must handle empty sources before calling this");
+
+ UncompressedSourceCache::AutoHoldEntry holder;
+
+ // UTF-8 source text.
+ if (hasSourceType<Utf8Unit>()) {
+ // Pass nullptr JSContext - this method is designed to be called
+ // off-main-thread where JSContext is not available. Decompression still
+ // works but without caching.
+ PinnedUnits<Utf8Unit> units(nullptr, this, holder, start, len);
+ if (!units.asChars()) {
+ // Allocation failure or decompression error.
+ return SubstringCharsResult(JS::UniqueChars(nullptr));
+ }
+
+ const char* str = units.asChars();
+ // For UTF-8 source, create a copy of the char data.
+ // Note: We allocate exactly `len` bytes without a null terminator.
+ // Callers must track the length separately.
+ char* copy = static_cast<char*>(js_malloc(len * sizeof(char)));
+ if (!copy) {
+ // Allocation failure.
+ return SubstringCharsResult(JS::UniqueChars(nullptr));
+ }
+
+ mozilla::PodCopy(copy, str, len);
+ return SubstringCharsResult(JS::UniqueChars(copy));
+ }
+
+ // UTF-16 source text.
+ // Pass nullptr JSContext - this method is designed to be called
+ // off-main-thread where JSContext is not available. Decompression still works
+ // but without caching.
+ PinnedUnits<char16_t> units(nullptr, this, holder, start, len);
+ if (!units.asChars()) {
+ // Allocation failure or decompression error.
+ return SubstringCharsResult(JS::UniqueTwoByteChars(nullptr));
+ }
+
+ // For UTF-16 source, create a copy of the char16_t data.
+ // Note: We allocate exactly `len` char16_t elements without a null
+ // terminator. Callers must track the length separately.
+ char16_t* copy = static_cast<char16_t*>(js_malloc(len * sizeof(char16_t)));
+ if (!copy) {
+ // Allocation failure.
+ return SubstringCharsResult(JS::UniqueTwoByteChars(nullptr));
+ }
+
+ mozilla::PodCopy(copy, units.asChars(), len);
+ return SubstringCharsResult(JS::UniqueTwoByteChars(copy));
+}
+
bool ScriptSource::appendSubstring(JSContext* cx, StringBuilder& buf,
size_t start, size_t stop) {
MOZ_ASSERT(start <= stop);
@@ -1387,6 +1503,23 @@ JSLinearString* ScriptSource::functionBodyString(JSContext* cx) {
return substring(cx, start, stop);
}
+SubstringCharsResult ScriptSource::functionBodyStringChars(size_t* outLength) {
+ MOZ_ASSERT(isFunctionBody());
+ MOZ_ASSERT(outLength);
+
+ size_t start = parameterListEnd_ + FunctionConstructorMedialSigils.length();
+ size_t stop = length() - FunctionConstructorFinalBrace.length();
+ *outLength = stop - start;
+
+ // Handle empty function body. Return nullptr to indicate empty result.
+ // This is distinct from substringChars which asserts non-empty length.
+ if (*outLength == 0) {
+ return SubstringCharsResult(JS::UniqueChars(nullptr));
+ }
+
+ return substringChars(start, stop);
+}
+
template <typename ContextT, typename Unit>
[[nodiscard]] bool ScriptSource::setUncompressedSourceHelper(
ContextT* cx, EntryUnits<Unit>&& source, size_t length,
diff --git a/js/src/vm/JSScript.h b/js/src/vm/JSScript.h
@@ -374,6 +374,11 @@ struct SourceTypeTraits<char16_t> {
[[nodiscard]] extern bool SynchronouslyCompressSource(
JSContext* cx, JS::Handle<BaseScript*> script);
+// Variant return type for ScriptSource::substringChars to support both UTF-8
+// and UTF-16.
+using SubstringCharsResult =
+ mozilla::Variant<JS::UniqueChars, JS::UniqueTwoByteChars>;
+
// [SMDOC] ScriptSource
//
// This class abstracts over the source we used to compile from. The current
@@ -418,7 +423,10 @@ class ScriptSource {
const Unit* units_;
public:
- PinnedUnits(JSContext* cx, ScriptSource* source,
+ // If maybeCx is nullptr, compressed sources will still be decompressed but
+ // the result will not be cached. This allows off-main-thread use without
+ // a JSContext.
+ PinnedUnits(JSContext* maybeCx, ScriptSource* source,
UncompressedSourceCache::AutoHoldEntry& holder, size_t begin,
size_t len);
@@ -615,8 +623,13 @@ class ScriptSource {
// How many ids have been handed out to sources.
static mozilla::Atomic<uint32_t, mozilla::SequentiallyConsistent> idCount_;
+ // Decompress and return the specified chunk of source code.
+ // If maybeCx is nullptr, decompression still works but the uncompressed
+ // result will not be cached. This allows off-main-thread callers to
+ // decompress source without a JSContext, at the cost of potentially
+ // decompressing the same chunk multiple times.
template <typename Unit>
- const Unit* chunkUnits(JSContext* cx,
+ const Unit* chunkUnits(JSContext* maybeCx,
UncompressedSourceCache::AutoHoldEntry& holder,
size_t chunk);
@@ -625,9 +638,13 @@ class ScriptSource {
//
// Warning: this is *not* GC-safe! Any chars to be handed out must use
// PinnedUnits. See comment below.
+ //
+ // If maybeCx is nullptr, compressed sources will still be decompressed but
+ // the result will not be cached. See chunkUnits comment above.
template <typename Unit>
- const Unit* units(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& asp,
- size_t begin, size_t len);
+ const Unit* units(JSContext* maybeCx,
+ UncompressedSourceCache::AutoHoldEntry& asp, size_t begin,
+ size_t len);
template <typename Unit>
const Unit* uncompressedUnits(size_t begin, size_t len);
@@ -663,7 +680,9 @@ class ScriptSource {
UniqueTwoByteChars&& str);
private:
+ class LoadSourceMatcherBase;
class LoadSourceMatcher;
+ class SourcePropertiesGetter;
public:
// Attempt to load usable source for |ss| -- source text on which substring
@@ -672,6 +691,16 @@ class ScriptSource {
// return false.
static bool loadSource(JSContext* cx, ScriptSource* ss, bool* loaded);
+ // This is similar to loadSource, but it is designed to be used outside of the
+ // main thread. This is done by removing the need of JSContext for the
+ // Retrievable sources that require sourceHook. For retrievable cases, it
+ // sets retrievable to true and sets the isUT16 depending on the encoding.
+ //
+ // *loaded indicates whether source text is available, *retrievable indicates
+ // whether the source can be retrieved later via source hook.
+ static void getSourceProperties(ScriptSource* ss, bool* hasSourceText,
+ bool* retrievable);
+
// Assign source data from |srcBuf| to this recently-created |ScriptSource|.
template <typename Unit>
[[nodiscard]] bool assignSource(FrontendContext* fc,
@@ -881,6 +910,18 @@ class ScriptSource {
JSLinearString* substring(JSContext* cx, size_t start, size_t stop);
JSLinearString* substringDontDeflate(JSContext* cx, size_t start,
size_t stop);
+ // Get substring characters without creating a JSString. Returns a variant
+ // containing either UniqueChars (UTF-8) or UniqueTwoByteChars (UTF-16).
+ //
+ // IMPORTANT: The returned buffer is NOT null-terminated. Callers must track
+ // the length separately (stop - start). This is designed for consumers that
+ // store length explicitly (e.g., ProfilerJSSourceData).
+ //
+ // Callers must handle empty sources before calling this (the function asserts
+ // non-empty length). Returns nullptr only on allocation failures. Designed
+ // for off-main-thread use where JSContext is not available for error
+ // reporting.
+ SubstringCharsResult substringChars(size_t start, size_t stop);
[[nodiscard]] bool appendSubstring(JSContext* cx, js::StringBuilder& buf,
size_t start, size_t stop);
@@ -889,9 +930,27 @@ class ScriptSource {
parameterListEnd_ = parameterListEnd;
}
- bool isFunctionBody() { return parameterListEnd_ != 0; }
+ bool isFunctionBody() const { return parameterListEnd_ != 0; }
JSLinearString* functionBodyString(JSContext* cx);
+ // Returns the function body substring. Unlike substringChars, this can return
+ // an empty result (nullptr with *outLength == 0) for empty function bodies.
+ // The caller doesn't need to check the length before calling.
+ SubstringCharsResult functionBodyStringChars(size_t* outLength);
+
+ // Returns true if this source should display only the function body.
+ // In case of DOM event handler like <div onclick="foo()" the JS code is
+ // wrapped into
+ // function onclick() {foo()}
+ // We want to only return `foo()` here.
+ // But only for event handlers, for `new Function("foo()")`, we want to
+ // return:
+ // function anonymous() {foo()}
+ bool shouldUnwrapEventHandlerBody() const {
+ return hasIntroductionType() &&
+ strcmp(introductionType(), "eventHandler") == 0 && isFunctionBody();
+ }
+
void addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
JS::ScriptSourceInfo* info) const;