/* -*- 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/. */
/*
* JS script operations.
*/
#include "vm/JSScript-inl.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Maybe.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Span.h" // mozilla::{Span,Span}
#include "mozilla/Sprintf.h"
#include "mozilla/Utf8.h"
#include "mozilla/Vector.h"
#include <algorithm>
#include <
new>
#include <string.h>
#include <type_traits>
#include <utility>
#include "jstypes.h"
#include "frontend/BytecodeSection.h"
#include "frontend/CompilationStencil.h" // frontend::CompilationStencil, frontend::InitialStencilAndDelazifications
#include "frontend/FrontendContext.h" // AutoReportFrontendContext
#include "frontend/ParseContext.h"
#include "frontend/SourceNotes.h" // SrcNote, SrcNoteType, SrcNoteIterator
#include "frontend/Stencil.h" // DumpFunctionFlagsItems, DumpImmutableScriptFlags
#include "frontend/StencilXdr.h" // XDRStencilEncoder
#include "gc/GCContext.h"
#include "jit/BaselineJIT.h"
#include "jit/CacheIRHealth.h"
#include "jit/Ion.h"
#include "jit/IonScript.h"
#include "jit/JitCode.h"
#include "jit/JitOptions.h"
#include "jit/JitRuntime.h"
#include "js/CharacterEncoding.h" // JS_EncodeStringToUTF8
#include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::ColumnNumberOneOrigin, JS::ColumnNumberOffset
#include "js/CompileOptions.h"
#include "js/experimental/SourceHook.h"
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/HeapAPI.h" // JS::GCCellPtr
#include "js/MemoryMetrics.h"
#include "js/Printer.h" // js::GenericPrinter, js::Fprinter, js::Sprinter, js::QuoteString
#include "js/Transcoding.h"
#include "js/UniquePtr.h"
#include "js/Utility.h" // JS::UniqueChars
#include "js/Value.h" // JS::Value
#include "util/Poison.h"
#include "util/StringBuilder.h"
#include "util/Text.h"
#include "vm/BigIntType.h" // JS::BigInt
#include "vm/BytecodeIterator.h"
#include "vm/BytecodeLocation.h"
#include "vm/BytecodeUtil.h" // Disassemble
#include "vm/Compression.h"
#include "vm/HelperThreadState.h" // js::RunPendingSourceCompressions
#include "vm/JSFunction.h"
#include "vm/JSObject.h"
#include "vm/JSONPrinter.h" // JSONPrinter
#include "vm/Opcodes.h"
#include "vm/PortableBaselineInterpret.h"
#include "vm/Scope.h" // Scope
#include "vm/SharedImmutableStringsCache.h"
#include "vm/StencilEnums.h" // TryNote, TryNoteKind, ScopeNote
#include "vm/StringType.h" // JSString, JSAtom
#include "vm/Time.h" // AutoIncrementalTimer
#include "vm/ToSource.h" // JS::ValueToSource
#ifdef MOZ_VTUNE
# include
"vtune/VTuneWrapper.h"
#endif
#include "gc/Marking-inl.h"
#include "vm/BytecodeIterator-inl.h"
#include "vm/BytecodeLocation-inl.h"
#include "vm/Compartment-inl.h"
#include "vm/JSContext-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/SharedImmutableStringsCache-inl.h"
#include "vm/Stack-inl.h"
using namespace js;
using mozilla::CheckedInt;
using mozilla::Maybe;
using mozilla::PointerRangeSize;
using mozilla::Utf8Unit;
using JS::ReadOnlyCompileOptions;
using JS::SourceText;
bool js::BaseScript::isUsingInterpreterTrampoline(JSRuntime* rt)
const {
return jitCodeRaw() == rt->jitRuntime()->interpreterStub().value;
}
js::ScriptSource* js::BaseScript::maybeForwardedScriptSource()
const {
return MaybeForwarded(sourceObject())->source();
}
void js::BaseScript::setEnclosingScript(BaseScript* enclosingScript) {
MOZ_ASSERT(enclosingScript);
warmUpData_.initEnclosingScript(enclosingScript);
}
void js::BaseScript::setEnclosingScope(Scope* enclosingScope) {
if (warmUpData_.isEnclosingScript()) {
warmUpData_.clearEnclosingScript();
}
MOZ_ASSERT(enclosingScope);
warmUpData_.initEnclosingScope(enclosingScope);
}
void js::BaseScript::finalize(JS::GCContext* gcx) {
// Scripts with bytecode may have optional data stored in per-runtime or
// per-zone maps. Note that a failed compilation must not have entries since
// the script itself will not be marked as having bytecode.
if (hasBytecode()) {
JSScript* script = this->asJSScript();
if (coverage::IsLCovEnabled()) {
coverage::CollectScriptCoverage(script,
true);
}
script->destroyScriptCounts();
}
{
JSRuntime* rt = gcx->runtime();
if (rt->hasJitRuntime() && rt->jitRuntime()->hasInterpreterEntryMap()) {
rt->jitRuntime()->getInterpreterEntryMap()->remove(
this);
}
rt->geckoProfiler().onScriptFinalized(
this);
}
#ifdef MOZ_VTUNE
if (zone()->scriptVTuneIdMap) {
// Note: we should only get here if the VTune JIT profiler is running.
zone()->scriptVTuneIdMap->remove(
this);
}
#endif
if (warmUpData_.isJitScript()) {
JSScript* script = this->asJSScript();
#ifdef JS_CACHEIR_SPEW
maybeUpdateWarmUpCount(script);
#endif
script->releaseJitScriptOnFinalize(gcx);
}
#ifdef JS_CACHEIR_SPEW
if (hasBytecode()) {
maybeSpewScriptFinalWarmUpCount(this->asJSScript());
}
#endif
if (data_) {
// We don't need to triger any barriers here, just free the memory.
size_t size = data_->allocationSize();
AlwaysPoison(data_, JS_POISONED_JSSCRIPT_DATA_PATTERN, size,
MemCheckKind::MakeNoAccess);
gcx->free_(
this, data_, size, MemoryUse::ScriptPrivateData);
}
freeSharedData();
}
js::Scope* js::BaseScript::releaseEnclosingScope() {
Scope* enclosing = warmUpData_.toEnclosingScope();
warmUpData_.clearEnclosingScope();
return enclosing;
}
void js::BaseScript::swapData(UniquePtr<PrivateScriptData>& other) {
if (data_) {
RemoveCellMemory(
this, data_->allocationSize(),
MemoryUse::ScriptPrivateData);
}
PrivateScriptData* old = data_;
data_.set(zone(), other.release());
other.reset(old);
if (data_) {
AddCellMemory(
this, data_->allocationSize(), MemoryUse::ScriptPrivateData);
}
}
js::Scope* js::BaseScript::enclosingScope()
const {
MOZ_ASSERT(!warmUpData_.isEnclosingScript(),
"Enclosing scope is not computed yet");
if (warmUpData_.isEnclosingScope()) {
return warmUpData_.toEnclosingScope();
}
MOZ_ASSERT(data_,
"Script doesn't seem to be compiled");
return gcthings()[js::GCThingIndex::outermostScopeIndex()]
.as<Scope>()
.enclosing();
}
size_t JSScript::numAlwaysLiveFixedSlots()
const {
if (bodyScope()->is<js::FunctionScope>()) {
return bodyScope()->as<js::FunctionScope>().nextFrameSlot();
}
if (bodyScope()->is<js::ModuleScope>()) {
return bodyScope()->as<js::ModuleScope>().nextFrameSlot();
}
if (bodyScope()->is<js::EvalScope>() &&
bodyScope()->kind() == ScopeKind::StrictEval) {
return bodyScope()->as<js::EvalScope>().nextFrameSlot();
}
return 0;
}
unsigned JSScript::numArgs()
const {
if (bodyScope()->is<js::FunctionScope>()) {
return bodyScope()->as<js::FunctionScope>().numPositionalFormalParameters();
}
return 0;
}
bool JSScript::functionHasParameterExprs()
const {
// Only functions have parameters.
js::Scope* scope = bodyScope();
if (!scope->is<js::FunctionScope>()) {
return false;
}
return scope->as<js::FunctionScope>().hasParameterExprs();
}
bool JSScript::isModule()
const {
return bodyScope()->is<js::ModuleScope>(); }
js::ModuleObject* JSScript::module()
const {
MOZ_ASSERT(isModule());
return bodyScope()->as<js::ModuleScope>().module();
}
bool JSScript::isGlobalCode()
const {
return bodyScope()->is<js::GlobalScope>();
}
js::VarScope* JSScript::functionExtraBodyVarScope()
const {
MOZ_ASSERT(functionHasExtraBodyVarScope());
for (JS::GCCellPtr gcThing : gcthings()) {
if (!gcThing.is<js::Scope>()) {
continue;
}
js::Scope* scope = &gcThing.as<js::Scope>();
if (scope->kind() == js::ScopeKind::FunctionBodyVar) {
return &scope->as<js::VarScope>();
}
}
MOZ_CRASH(
"Function extra body var scope not found");
}
bool JSScript::needsBodyEnvironment()
const {
for (JS::GCCellPtr gcThing : gcthings()) {
if (!gcThing.is<js::Scope>()) {
continue;
}
js::Scope* scope = &gcThing.as<js::Scope>();
if (ScopeKindIsInBody(scope->kind()) && scope->hasEnvironment()) {
return true;
}
}
return false;
}
bool JSScript::isDirectEvalInFunction()
const {
if (!isForEval()) {
return false;
}
return bodyScope()->hasOnChain(js::ScopeKind::Function);
}
// Initialize the optional arrays in the trailing allocation. This is a set of
// offsets that delimit each optional array followed by the arrays themselves.
// See comment before 'ImmutableScriptData' for more details.
void ImmutableScriptData::initOptionalArrays(Offset* pcursor,
uint32_t numResumeOffsets,
uint32_t numScopeNotes,
uint32_t numTryNotes) {
Offset cursor = (*pcursor);
// The byte arrays must have already been padded.
MOZ_ASSERT(isAlignedOffset<CodeNoteAlign>(cursor),
"Bytecode and source notes should be padded to keep alignment");
// Each non-empty optional array needs will need an offset to its end.
unsigned numOptionalArrays =
unsigned(numResumeOffsets > 0) +
unsigned(numScopeNotes > 0) +
unsigned(numTryNotes > 0);
// Default-initialize the optional-offsets.
initElements<Offset>(cursor, numOptionalArrays);
cursor += numOptionalArrays *
sizeof(Offset);
// Offset between optional-offsets table and the optional arrays. This is
// later used to access the optional-offsets table as well as first optional
// array.
optArrayOffset_ = cursor;
// Each optional array that follows must store an end-offset in the offset
// table. Assign table entries by using this 'offsetIndex'. The index 0 is
// reserved for implicit value 'optArrayOffset'.
int offsetIndex = 0;
// Default-initialize optional 'resumeOffsets'.
MOZ_ASSERT(resumeOffsetsOffset() == cursor);
if (numResumeOffsets > 0) {
initElements<uint32_t>(cursor, numResumeOffsets);
cursor += numResumeOffsets *
sizeof(uint32_t);
setOptionalOffset(++offsetIndex, cursor);
}
flagsRef().resumeOffsetsEndIndex = offsetIndex;
// Default-initialize optional 'scopeNotes'.
MOZ_ASSERT(scopeNotesOffset() == cursor);
if (numScopeNotes > 0) {
initElements<ScopeNote>(cursor, numScopeNotes);
cursor += numScopeNotes *
sizeof(ScopeNote);
setOptionalOffset(++offsetIndex, cursor);
}
flagsRef().scopeNotesEndIndex = offsetIndex;
// Default-initialize optional 'tryNotes'
MOZ_ASSERT(tryNotesOffset() == cursor);
if (numTryNotes > 0) {
initElements<TryNote>(cursor, numTryNotes);
cursor += numTryNotes *
sizeof(TryNote);
setOptionalOffset(++offsetIndex, cursor);
}
flagsRef().tryNotesEndIndex = offsetIndex;
MOZ_ASSERT(endOffset() == cursor);
(*pcursor) = cursor;
}
ImmutableScriptData::ImmutableScriptData(uint32_t codeLength,
uint32_t noteLength,
uint32_t numResumeOffsets,
uint32_t numScopeNotes,
uint32_t numTryNotes)
: codeLength_(codeLength) {
// Variable-length data begins immediately after ImmutableScriptData itself.
Offset cursor =
sizeof(ImmutableScriptData);
// The following arrays are byte-aligned with additional padding to ensure
// that together they maintain uint32_t-alignment.
{
MOZ_ASSERT(isAlignedOffset<CodeNoteAlign>(cursor));
// Zero-initialize 'flags'
MOZ_ASSERT(isAlignedOffset<Flags>(cursor));
new (offsetToPointer<
void>(cursor)) Flags{};
cursor +=
sizeof(Flags);
initElements<jsbytecode>(cursor, codeLength);
cursor += codeLength *
sizeof(jsbytecode);
initElements<SrcNote>(cursor, noteLength);
cursor += noteLength *
sizeof(SrcNote);
MOZ_ASSERT(isAlignedOffset<CodeNoteAlign>(cursor));
}
// Initialization for remaining arrays.
initOptionalArrays(&cursor, numResumeOffsets, numScopeNotes, numTryNotes);
// Check that we correctly recompute the expected values.
MOZ_ASSERT(this->codeLength() == codeLength);
MOZ_ASSERT(this->noteLength() == noteLength);
// Sanity check
MOZ_ASSERT(endOffset() == cursor);
}
void js::FillImmutableFlagsFromCompileOptionsForTopLevel(
const ReadOnlyCompileOptions& options, ImmutableScriptFlags& flags) {
using ImmutableFlags = ImmutableScriptFlagsEnum;
js::FillImmutableFlagsFromCompileOptionsForFunction(options, flags);
flags.setFlag(ImmutableFlags::TreatAsRunOnce, options.isRunOnce);
flags.setFlag(ImmutableFlags::NoScriptRval, options.noScriptRval);
}
void js::FillImmutableFlagsFromCompileOptionsForFunction(
const ReadOnlyCompileOptions& options, ImmutableScriptFlags& flags) {
using ImmutableFlags = ImmutableScriptFlagsEnum;
flags.setFlag(ImmutableFlags::SelfHosted, options.selfHostingMode);
flags.setFlag(ImmutableFlags::ForceStrict, options.forceStrictMode());
flags.setFlag(ImmutableFlags::HasNonSyntacticScope,
options.nonSyntacticScope);
}
// Check if flags matches to compile options for flags set by
// FillImmutableFlagsFromCompileOptionsForTopLevel above.
bool js::CheckCompileOptionsMatch(
const ReadOnlyCompileOptions& options,
ImmutableScriptFlags flags) {
using ImmutableFlags = ImmutableScriptFlagsEnum;
bool selfHosted = !!(flags & uint32_t(ImmutableFlags::SelfHosted));
bool forceStrict = !!(flags & uint32_t(ImmutableFlags::ForceStrict));
bool hasNonSyntacticScope =
!!(flags & uint32_t(ImmutableFlags::HasNonSyntacticScope));
bool noScriptRval = !!(flags & uint32_t(ImmutableFlags::NoScriptRval));
bool treatAsRunOnce = !!(flags & uint32_t(ImmutableFlags::TreatAsRunOnce));
return options.selfHostingMode == selfHosted &&
options.noScriptRval == noScriptRval &&
options.isRunOnce == treatAsRunOnce &&
options.forceStrictMode() == forceStrict &&
options.nonSyntacticScope == hasNonSyntacticScope;
}
JS_PUBLIC_API
bool JS::CheckCompileOptionsMatch(
const ReadOnlyCompileOptions& options, JSScript* script) {
return js::CheckCompileOptionsMatch(options, script->immutableFlags());
}
bool JSScript::initScriptCounts(JSContext* cx) {
MOZ_ASSERT(!hasScriptCounts());
// Record all pc which are the first instruction of a basic block.
mozilla::Vector<jsbytecode*, 16, SystemAllocPolicy> jumpTargets;
js::BytecodeLocation main = mainLocation();
AllBytecodesIterable iterable(
this);
for (
auto& loc : iterable) {
if (loc.isJumpTarget() || loc == main) {
if (!jumpTargets.append(loc.toRawBytecode())) {
ReportOutOfMemory(cx);
return false;
}
}
}
// Initialize all PCCounts counters to 0.
ScriptCounts::PCCountsVector base;
if (!base.reserve(jumpTargets.length())) {
ReportOutOfMemory(cx);
return false;
}
for (size_t i = 0; i < jumpTargets.length(); i++) {
base.infallibleEmplaceBack(pcToOffset(jumpTargets[i]));
}
// Create zone's scriptCountsMap if necessary.
if (!zone()->scriptCountsMap) {
auto map = cx->make_unique<ScriptCountsMap>();
if (!map) {
return false;
}
zone()->scriptCountsMap = std::move(map);
}
// Allocate the ScriptCounts.
UniqueScriptCounts sc = cx->make_unique<ScriptCounts>(std::move(base));
if (!sc) {
return false;
}
MOZ_ASSERT(this->hasBytecode());
// Register the current ScriptCounts in the zone's map.
if (!zone()->scriptCountsMap->putNew(
this, std::move(sc))) {
ReportOutOfMemory(cx);
return false;
}
// safe to set this; we can't fail after this point.
setHasScriptCounts();
// Enable interrupts in any interpreter frames running on this script. This
// is used to let the interpreter increment the PCCounts, if present.
for (ActivationIterator iter(cx); !iter.done(); ++iter) {
if (iter->isInterpreter()) {
iter->asInterpreter()->enableInterruptsIfRunning(
this);
}
}
return true;
}
static inline ScriptCountsMap::Ptr GetScriptCountsMapEntry(JSScript* script) {
MOZ_ASSERT(script->hasScriptCounts());
ScriptCountsMap::Ptr p = script->zone()->scriptCountsMap->lookup(script);
MOZ_ASSERT(p);
return p;
}
ScriptCounts& JSScript::getScriptCounts() {
ScriptCountsMap::Ptr p = GetScriptCountsMapEntry(
this);
return *p->value();
}
js::PCCounts* ScriptCounts::maybeGetPCCounts(size_t offset) {
PCCounts searched = PCCounts(offset);
PCCounts* elem =
std::lower_bound(pcCounts_.begin(), pcCounts_.end(), searched);
if (elem == pcCounts_.end() || elem->pcOffset() != offset) {
return nullptr;
}
return elem;
}
const js::PCCounts* ScriptCounts::maybeGetPCCounts(size_t offset)
const {
PCCounts searched = PCCounts(offset);
const PCCounts* elem =
std::lower_bound(pcCounts_.begin(), pcCounts_.end(), searched);
if (elem == pcCounts_.end() || elem->pcOffset() != offset) {
return nullptr;
}
return elem;
}
js::PCCounts* ScriptCounts::getImmediatePrecedingPCCounts(size_t offset) {
PCCounts searched = PCCounts(offset);
PCCounts* elem =
std::lower_bound(pcCounts_.begin(), pcCounts_.end(), searched);
if (elem == pcCounts_.end()) {
return &pcCounts_.back();
}
if (elem->pcOffset() == offset) {
return elem;
}
if (elem != pcCounts_.begin()) {
return elem - 1;
}
return nullptr;
}
const js::PCCounts* ScriptCounts::maybeGetThrowCounts(size_t offset)
const {
PCCounts searched = PCCounts(offset);
const PCCounts* elem =
std::lower_bound(throwCounts_.begin(), throwCounts_.end(), searched);
if (elem == throwCounts_.end() || elem->pcOffset() != offset) {
return nullptr;
}
return elem;
}
const js::PCCounts* ScriptCounts::getImmediatePrecedingThrowCounts(
size_t offset)
const {
PCCounts searched = PCCounts(offset);
const PCCounts* elem =
std::lower_bound(throwCounts_.begin(), throwCounts_.end(), searched);
if (elem == throwCounts_.end()) {
if (throwCounts_.begin() == throwCounts_.end()) {
return nullptr;
}
return &throwCounts_.back();
}
if (elem->pcOffset() == offset) {
return elem;
}
if (elem != throwCounts_.begin()) {
return elem - 1;
}
return nullptr;
}
js::PCCounts* ScriptCounts::getThrowCounts(size_t offset) {
PCCounts searched = PCCounts(offset);
PCCounts* elem =
std::lower_bound(throwCounts_.begin(), throwCounts_.end(), searched);
if (elem == throwCounts_.end() || elem->pcOffset() != offset) {
elem = throwCounts_.insert(elem, searched);
}
return elem;
}
size_t ScriptCounts::sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) {
size_t size = mallocSizeOf(
this);
size += pcCounts_.sizeOfExcludingThis(mallocSizeOf);
size += throwCounts_.sizeOfExcludingThis(mallocSizeOf);
if (ionCounts_) {
size += ionCounts_->sizeOfIncludingThis(mallocSizeOf);
}
return size;
}
js::PCCounts* JSScript::maybeGetPCCounts(jsbytecode* pc) {
MOZ_ASSERT(containsPC(pc));
return getScriptCounts().maybeGetPCCounts(pcToOffset(pc));
}
const js::PCCounts* JSScript::maybeGetThrowCounts(jsbytecode* pc) {
MOZ_ASSERT(containsPC(pc));
return getScriptCounts().maybeGetThrowCounts(pcToOffset(pc));
}
js::PCCounts* JSScript::getThrowCounts(jsbytecode* pc) {
MOZ_ASSERT(containsPC(pc));
return getScriptCounts().getThrowCounts(pcToOffset(pc));
}
uint64_t JSScript::getHitCount(jsbytecode* pc) {
MOZ_ASSERT(containsPC(pc));
if (pc < main()) {
pc = main();
}
ScriptCounts& sc = getScriptCounts();
size_t targetOffset = pcToOffset(pc);
const js::PCCounts* baseCount =
sc.getImmediatePrecedingPCCounts(targetOffset);
if (!baseCount) {
return 0;
}
if (baseCount->pcOffset() == targetOffset) {
return baseCount->numExec();
}
MOZ_ASSERT(baseCount->pcOffset() < targetOffset);
uint64_t count = baseCount->numExec();
do {
const js::PCCounts* throwCount =
sc.getImmediatePrecedingThrowCounts(targetOffset);
if (!throwCount) {
return count;
}
if (throwCount->pcOffset() <= baseCount->pcOffset()) {
return count;
}
count -= throwCount->numExec();
targetOffset = throwCount->pcOffset() - 1;
}
while (
true);
}
void JSScript::addIonCounts(jit::IonScriptCounts* ionCounts) {
ScriptCounts& sc = getScriptCounts();
if (sc.ionCounts_) {
ionCounts->setPrevious(sc.ionCounts_);
}
sc.ionCounts_ = ionCounts;
}
jit::IonScriptCounts* JSScript::getIonCounts() {
return getScriptCounts().ionCounts_;
}
void JSScript::releaseScriptCounts(ScriptCounts* counts) {
ScriptCountsMap::Ptr p = GetScriptCountsMapEntry(
this);
*counts = std::move(*p->value().get());
zone()->scriptCountsMap->remove(p);
clearHasScriptCounts();
}
void JSScript::destroyScriptCounts() {
if (hasScriptCounts()) {
ScriptCounts scriptCounts;
releaseScriptCounts(&scriptCounts);
}
}
void JSScript::resetScriptCounts() {
if (!hasScriptCounts()) {
return;
}
ScriptCounts& sc = getScriptCounts();
for (PCCounts& elem : sc.pcCounts_) {
elem.numExec() = 0;
}
for (PCCounts& elem : sc.throwCounts_) {
elem.numExec() = 0;
}
}
void ScriptSourceObject::finalize(JS::GCContext* gcx, JSObject* obj) {
MOZ_ASSERT(gcx->onMainThread());
ScriptSourceObject* sso = &obj->as<ScriptSourceObject>();
sso->source()->Release();
// Clear the private value, calling the release hook if necessary.
sso->setPrivate(gcx->runtime(), UndefinedValue());
sso->clearStencils();
}
static const JSClassOps ScriptSourceObjectClassOps = {
nullptr,
// addProperty
nullptr,
// delProperty
nullptr,
// enumerate
nullptr,
// newEnumerate
nullptr,
// resolve
nullptr,
// mayResolve
ScriptSourceObject::finalize,
// finalize
nullptr,
// call
nullptr,
// construct
nullptr,
// trace
};
const JSClass ScriptSourceObject::class_ = {
"ScriptSource",
JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS) | JSCLASS_FOREGROUND_FINALIZE,
&ScriptSourceObjectClassOps,
};
ScriptSourceObject* ScriptSourceObject::create(JSContext* cx,
ScriptSource* source) {
ScriptSourceObject* obj =
NewObjectWithGivenProto<ScriptSourceObject>(cx, nullptr);
if (!obj) {
return nullptr;
}
// The matching decref is in ScriptSourceObject::finalize.
obj->initReservedSlot(SOURCE_SLOT, PrivateValue(do_AddRef(source).take()));
// The slots below should be populated by a call to initFromOptions. Poison
// them.
obj->initReservedSlot(ELEMENT_PROPERTY_SLOT, MagicValue(JS_GENERIC_MAGIC));
obj->initReservedSlot(INTRODUCTION_SCRIPT_SLOT, MagicValue(JS_GENERIC_MAGIC));
obj->initReservedSlot(STENCILS_SLOT, UndefinedValue());
return obj;
}
[[nodiscard]]
static bool MaybeValidateFilename(
JSContext* cx, Handle<ScriptSourceObject*> sso,
const JS::InstantiateOptions& options) {
if (!gFilenameValidationCallback) {
return true;
}
const char* filename = sso->source()->filename();
if (!filename || options.skipFilenameValidation) {
return true;
}
if (gFilenameValidationCallback(cx, filename)) {
return true;
}
const char* utf8Filename;
if (mozilla::IsUtf8(mozilla::MakeStringSpan(filename))) {
utf8Filename = filename;
}
else {
utf8Filename =
"(invalid UTF-8 filename)";
}
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_UNSAFE_FILENAME,
utf8Filename);
return false;
}
/* static */
bool ScriptSourceObject::initFromOptions(
JSContext* cx, Handle<ScriptSourceObject*> source,
const JS::InstantiateOptions& options) {
cx->releaseCheck(source);
MOZ_ASSERT(
source->getReservedSlot(ELEMENT_PROPERTY_SLOT).isMagic(JS_GENERIC_MAGIC));
MOZ_ASSERT(source->getReservedSlot(INTRODUCTION_SCRIPT_SLOT)
.isMagic(JS_GENERIC_MAGIC));
if (!MaybeValidateFilename(cx, source, options)) {
return false;
}
if (options.deferDebugMetadata) {
return true;
}
// Initialize the element attribute slot and introduction script slot
// this marks the SSO as initialized for asserts.
RootedString elementAttributeName(cx);
if (!initElementProperties(cx, source, elementAttributeName)) {
return false;
}
RootedValue introductionScript(cx);
source->setReservedSlot(INTRODUCTION_SCRIPT_SLOT, introductionScript);
return true;
}
/* static */
bool ScriptSourceObject::initElementProperties(
JSContext* cx, Handle<ScriptSourceObject*> source,
HandleString elementAttrName) {
RootedValue nameValue(cx);
if (elementAttrName) {
nameValue = StringValue(elementAttrName);
}
if (!cx->compartment()->wrap(cx, &nameValue)) {
return false;
}
source->setReservedSlot(ELEMENT_PROPERTY_SLOT, nameValue);
return true;
}
void ScriptSourceObject::setPrivate(JSRuntime* rt,
const Value& value) {
// Update the private value, calling addRef/release hooks if necessary
// to allow the embedding to maintain a reference count for the
// private data.
JS::AutoSuppressGCAnalysis nogc;
Value prevValue = getReservedSlot(PRIVATE_SLOT);
rt->releaseScriptPrivate(prevValue);
setReservedSlot(PRIVATE_SLOT, value);
rt->addRefScriptPrivate(value);
}
void ScriptSourceObject::clearPrivate(JSRuntime* rt) {
// Clear the private value, calling release hook if necessary.
// |this| may be gray, be careful not to create edges to it.
JS::AutoSuppressGCAnalysis nogc;
Value prevValue = getReservedSlot(PRIVATE_SLOT);
rt->releaseScriptPrivate(prevValue);
getSlotRef(PRIVATE_SLOT).setUndefinedUnchecked();
}
class ScriptSource::LoadSourceMatcher {
JSContext*
const cx_;
ScriptSource*
const ss_;
bool*
const loaded_;
public:
explicit LoadSourceMatcher(JSContext* cx, ScriptSource* ss,
bool* loaded)
: cx_(cx), ss_(ss), loaded_(loaded) {}
template <
typename Unit, SourceRetrievable CanRetrieve>
bool operator()(
const Compressed<Unit, CanRetrieve>&)
const {
*loaded_ =
true;
return true;
}
template <
typename Unit, SourceRetrievable CanRetrieve>
bool operator()(
const Uncompressed<Unit, CanRetrieve>&)
const {
*loaded_ =
true;
return true;
}
template <
typename Unit>
bool operator()(
const Retrievable<Unit>&) {
if (!cx_->runtime()->sourceHook.ref()) {
*loaded_ =
false;
return true;
}
size_t length;
// The first argument is just for overloading -- its value doesn't matter.
if (!tryLoadAndSetSource(Unit(
'0'), &length)) {
return false;
}
return true;
}
bool operator()(
const Missing&)
const {
*loaded_ =
false;
return true;
}
private:
bool tryLoadAndSetSource(
const Utf8Unit&, size_t* length)
const {
char* utf8Source;
if (!cx_->runtime()->sourceHook->load(cx_, ss_->filename(), nullptr,
&utf8Source, length)) {
return false;
}
if (!utf8Source) {
*loaded_ =
false;
return true;
}
if (!ss_->setRetrievedSource(
cx_, EntryUnits<Utf8Unit>(
reinterpret_cast<Utf8Unit*>(utf8Source)),
*length)) {
return false;
}
*loaded_ =
true;
return true;
}
bool tryLoadAndSetSource(
const char16_t&, size_t* length)
const {
char16_t* utf16Source;
if (!cx_->runtime()->sourceHook->load(cx_, ss_->filename(), &utf16Source,
nullptr, length)) {
return false;
}
if (!utf16Source) {
*loaded_ =
false;
return true;
}
if (!ss_->setRetrievedSource(cx_, EntryUnits<char16_t>(utf16Source),
*length)) {
return false;
}
*loaded_ =
true;
return true;
}
};
/* static */
bool ScriptSource::loadSource(JSContext* cx, ScriptSource* ss,
bool* loaded) {
return ss->data.match(LoadSourceMatcher(cx, ss, loaded));
}
/* static */
JSLinearString* JSScript::sourceData(JSContext* cx, HandleScript script) {
MOZ_ASSERT(script->scriptSource()->hasSourceText());
return script->scriptSource()->substring(cx, script->sourceStart(),
script->sourceEnd());
}
bool BaseScript::appendSourceDataForToString(JSContext* cx,
StringBuilder& buf) {
MOZ_ASSERT(scriptSource()->hasSourceText());
return scriptSource()->appendSubstring(cx, buf, toStringStart(),
toStringEnd());
}
void UncompressedSourceCache::holdEntry(AutoHoldEntry& holder,
const ScriptSourceChunk& ssc) {
MOZ_ASSERT(!holder_);
holder.holdEntry(
this, ssc);
holder_ = &holder;
}
void UncompressedSourceCache::releaseEntry(AutoHoldEntry& holder) {
MOZ_ASSERT(holder_ == &holder);
holder_ = nullptr;
}
template <
typename Unit>
const Unit* UncompressedSourceCache::lookup(
const ScriptSourceChunk& ssc,
AutoHoldEntry& holder) {
MOZ_ASSERT(!holder_);
MOZ_ASSERT(ssc.ss->isCompressed<Unit>());
if (!map_) {
return nullptr;
}
if (Map::Ptr p = map_->lookup(ssc)) {
holdEntry(holder, ssc);
return static_cast<
const Unit*>(p->value().get());
}
return nullptr;
}
bool UncompressedSourceCache::put(
const ScriptSourceChunk& ssc, SourceData data,
AutoHoldEntry& holder) {
MOZ_ASSERT(!holder_);
if (!map_) {
map_ = MakeUnique<Map>();
if (!map_) {
return false;
}
}
if (!map_->put(ssc, std::move(data))) {
return false;
}
holdEntry(holder, ssc);
return true;
}
void UncompressedSourceCache::purge() {
if (!map_) {
return;
}
for (Map::Range r = map_->all(); !r.empty(); r.popFront()) {
if (holder_ && r.front().key() == holder_->sourceChunk()) {
holder_->deferDelete(std::move(r.front().value()));
holder_ = nullptr;
}
}
map_ = nullptr;
}
size_t UncompressedSourceCache::sizeOfExcludingThis(
mozilla::MallocSizeOf mallocSizeOf) {
size_t n = 0;
if (map_ && !map_->empty()) {
n += map_->shallowSizeOfIncludingThis(mallocSizeOf);
for (Map::Range r = map_->all(); !r.empty(); r.popFront()) {
n += mallocSizeOf(r.front().value().get());
}
}
return n;
}
template <
typename Unit>
const Unit* ScriptSource::chunkUnits(
JSContext* cx, 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;
}
size_t totalLengthInBytes = length() *
sizeof(Unit);
size_t chunkBytes = Compressor::chunkSize(totalLengthInBytes, chunk);
MOZ_ASSERT((chunkBytes %
sizeof(Unit)) == 0);
const size_t chunkLength = chunkBytes /
sizeof(Unit);
EntryUnits<Unit> decompressed(js_pod_malloc<Unit>(chunkLength));
if (!decompressed) {
JS_ReportOutOfMemory(cx);
return nullptr;
}
// Compression treats input and output memory as plain ol' bytes. These
// reinterpret_cast<>s accord exactly with that.
if (!DecompressStringChunk(
reinterpret_cast<
const unsigned char*>(c.raw.chars()), chunk,
reinterpret_cast<
unsigned char*>(decompressed.get()), chunkBytes)) {
JS_ReportOutOfMemory(cx);
return nullptr;
}
const Unit* ret = decompressed.get();
if (!cx->caches().uncompressedSourceCache.put(
ssc, ToSourceData(std::move(decompressed)), holder)) {
JS_ReportOutOfMemory(cx);
return nullptr;
}
return ret;
}
template <
typename Unit>
void ScriptSource::convertToCompressedSource(SharedImmutableString compressed,
size_t uncompressedLength) {
MOZ_ASSERT(isUncompressed<Unit>());
MOZ_ASSERT(uncompressedData<Unit>()->length() == uncompressedLength);
if (data.is<Uncompressed<Unit, SourceRetrievable::Yes>>()) {
data = SourceType(Compressed<Unit, SourceRetrievable::Yes>(
std::move(compressed), uncompressedLength));
}
else {
data = SourceType(Compressed<Unit, SourceRetrievable::No>(
std::move(compressed), uncompressedLength));
}
}
template <
typename Unit>
void ScriptSource::performDelayedConvertToCompressedSource(
ExclusiveData<ReaderInstances>::Guard& g) {
// There might not be a conversion to compressed source happening at all.
if (g->pendingCompressed.empty()) {
return;
}
CompressedData<Unit>& pending =
g->pendingCompressed.ref<CompressedData<Unit>>();
convertToCompressedSource<Unit>(std::move(pending.raw),
pending.uncompressedLength);
g->pendingCompressed.destroy();
}
void ScriptSource::PinnedUnitsBase::addReader() {
auto guard = source_->readers_.lock();
guard->count++;
}
template <
typename Unit>
void ScriptSource::PinnedUnitsBase::removeReader() {
// If the off-thread compression task couldn't perform
// convertToCompressedSource, the conversion is pending on
// the pendingCompressed field.
//
// If there's no other reader at this point, perform the pending conversion
// here.
//
// See also ScriptSource::triggerConvertToCompressedSource.
auto guard = source_->readers_.lock();
MOZ_ASSERT(guard->count > 0);
if (--guard->count == 0) {
source_->performDelayedConvertToCompressedSource<Unit>(guard);
}
}
template <
typename Unit>
ScriptSource::PinnedUnits<Unit>::~PinnedUnits() {
if (units_) {
removeReader<Unit>();
}
}
template <
typename Unit>
ScriptSource::PinnedUnitsIfUncompressed<Unit>::~PinnedUnitsIfUncompressed() {
if (units_) {
removeReader<Unit>();
}
}
template <
typename Unit>
const Unit* ScriptSource::units(JSContext* cx,
UncompressedSourceCache::AutoHoldEntry& holder,
size_t begin, size_t len) {
MOZ_ASSERT(begin <= length());
MOZ_ASSERT(begin + len <= length());
if (isUncompressed<Unit>()) {
const Unit* units = uncompressedData<Unit>()->units();
if (!units) {
return nullptr;
}
return units + begin;
}
if (data.is<Missing>()) {
MOZ_CRASH(
"ScriptSource::units() on ScriptSource with missing source");
}
if (data.is<Retrievable<Unit>>()) {
MOZ_CRASH(
"ScriptSource::units() on ScriptSource with retrievable source");
}
MOZ_ASSERT(isCompressed<Unit>());
// Determine first/last chunks, the offset (in bytes) into the first chunk
// of the requested units, and the number of bytes in the last chunk.
//
// Note that first and last chunk sizes are miscomputed and *must not be
// used* when the first chunk is the last chunk.
size_t firstChunk, firstChunkOffset, firstChunkSize;
size_t lastChunk, lastChunkSize;
Compressor::rangeToChunkAndOffset(
begin *
sizeof(Unit), (begin + len) *
sizeof(Unit), &firstChunk,
&firstChunkOffset, &firstChunkSize, &lastChunk, &lastChunkSize);
MOZ_ASSERT(firstChunk <= lastChunk);
MOZ_ASSERT(firstChunkOffset %
sizeof(Unit) == 0);
MOZ_ASSERT(firstChunkSize %
sizeof(Unit) == 0);
size_t firstUnit = firstChunkOffset /
sizeof(Unit);
// 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);
if (!units) {
return nullptr;
}
return units + firstUnit;
}
// Otherwise the units span multiple chunks. Copy successive chunks'
// decompressed units into freshly-allocated memory to return.
EntryUnits<Unit> decompressed(js_pod_malloc<Unit>(len));
if (!decompressed) {
JS_ReportOutOfMemory(cx);
return nullptr;
}
Unit* cursor;
{
// |AutoHoldEntry| is single-shot, and a holder successfully filled in
// by |chunkUnits| must be destroyed before another can be used. Thus
// we can't use |holder| with |chunkUnits| when |chunkUnits| is used
// 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);
if (!units) {
return nullptr;
}
cursor = std::copy_n(units + firstUnit, firstChunkSize /
sizeof(Unit),
decompressed.get());
}
for (size_t i = firstChunk + 1; i < lastChunk; i++) {
UncompressedSourceCache::AutoHoldEntry chunkHolder;
const Unit* units = chunkUnits<Unit>(cx, chunkHolder, i);
if (!units) {
return nullptr;
}
cursor = std::copy_n(units, Compressor::CHUNK_SIZE /
sizeof(Unit), cursor);
}
{
UncompressedSourceCache::AutoHoldEntry lastHolder;
const Unit* units = chunkUnits<Unit>(cx, lastHolder, lastChunk);
if (!units) {
return nullptr;
}
cursor = std::copy_n(units, lastChunkSize /
sizeof(Unit), cursor);
}
MOZ_ASSERT(PointerRangeSize(decompressed.get(), cursor) == len);
// Transfer ownership to |holder|.
const Unit* ret = decompressed.get();
holder.holdUnits(std::move(decompressed));
return ret;
}
template <
typename Unit>
const Unit* ScriptSource::uncompressedUnits(size_t begin, size_t len) {
MOZ_ASSERT(begin <= length());
MOZ_ASSERT(begin + len <= length());
if (!isUncompressed<Unit>()) {
return nullptr;
}
const Unit* units = uncompressedData<Unit>()->units();
if (!units) {
return nullptr;
}
return units + begin;
}
template <
typename Unit>
ScriptSource::PinnedUnits<Unit>::PinnedUnits(
JSContext* cx, 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);
if (!units_) {
removeReader<Unit>();
}
}
template class ScriptSource::PinnedUnits<Utf8Unit>;
template class ScriptSource::PinnedUnits<char16_t>;
template <
typename Unit>
ScriptSource::PinnedUnitsIfUncompressed<Unit>::PinnedUnitsIfUncompressed(
ScriptSource* source, size_t begin, size_t len)
: PinnedUnitsBase(source) {
MOZ_ASSERT(source->hasSourceType<Unit>(),
"must pin units of source's type");
addReader();
units_ = source->uncompressedUnits<Unit>(begin, len);
if (!units_) {
removeReader<Unit>();
}
}
template class ScriptSource::PinnedUnitsIfUncompressed<Utf8Unit>;
template class ScriptSource::PinnedUnitsIfUncompressed<char16_t>;
JSLinearString* ScriptSource::substring(JSContext* cx, size_t start,
size_t stop) {
MOZ_ASSERT(start <= stop);
size_t len = stop - start;
if (!len) {
return cx->emptyString();
}
UncompressedSourceCache::AutoHoldEntry holder;
// UTF-8 source text.
if (hasSourceType<Utf8Unit>()) {
PinnedUnits<Utf8Unit> units(cx,
this, holder, start, len);
if (!units.asChars()) {
return nullptr;
}
const char* str = units.asChars();
return NewStringCopyUTF8N(cx, JS::UTF8Chars(str, len));
}
// UTF-16 source text.
PinnedUnits<char16_t> units(cx,
this, holder, start, len);
if (!units.asChars()) {
return nullptr;
}
return NewStringCopyN<CanGC>(cx, units.asChars(), len);
}
JSLinearString* ScriptSource::substringDontDeflate(JSContext* cx, size_t start,
size_t stop) {
MOZ_ASSERT(start <= stop);
size_t len = stop - start;
if (!len) {
return cx->emptyString();
}
UncompressedSourceCache::AutoHoldEntry holder;
// UTF-8 source text.
if (hasSourceType<Utf8Unit>()) {
PinnedUnits<Utf8Unit> units(cx,
this, holder, start, len);
if (!units.asChars()) {
return nullptr;
}
const char* str = units.asChars();
// There doesn't appear to be a non-deflating UTF-8 string creation
// function -- but then again, it's not entirely clear how current
// callers benefit from non-deflation.
return NewStringCopyUTF8N(cx, JS::UTF8Chars(str, len));
}
// UTF-16 source text.
PinnedUnits<char16_t> units(cx,
this, holder, start, len);
if (!units.asChars()) {
return nullptr;
}
return NewStringCopyNDontDeflate<CanGC>(cx, units.asChars(), len);
}
bool ScriptSource::appendSubstring(JSContext* cx, StringBuilder& buf,
size_t start, size_t stop) {
MOZ_ASSERT(start <= stop);
size_t len = stop - start;
UncompressedSourceCache::AutoHoldEntry holder;
if (hasSourceType<Utf8Unit>()) {
PinnedUnits<Utf8Unit> pinned(cx,
this, holder, start, len);
if (!pinned.get()) {
return false;
}
if (len > SourceDeflateLimit && !buf.ensureTwoByteChars()) {
return false;
}
const Utf8Unit* units = pinned.get();
return buf.append(units, len);
}
else {
PinnedUnits<char16_t> pinned(cx,
this, holder, start, len);
if (!pinned.get()) {
return false;
}
if (len > SourceDeflateLimit && !buf.ensureTwoByteChars()) {
return false;
}
const char16_t* units = pinned.get();
return buf.append(units, len);
}
}
JSLinearString* ScriptSource::functionBodyString(JSContext* cx) {
MOZ_ASSERT(isFunctionBody());
size_t start = parameterListEnd_ + FunctionConstructorMedialSigils.length();
size_t stop = length() - FunctionConstructorFinalBrace.length();
return substring(cx, start, stop);
}
template <
typename ContextT,
typename Unit>
[[nodiscard]]
bool ScriptSource::setUncompressedSourceHelper(
ContextT* cx, EntryUnits<Unit>&& source, size_t length,
SourceRetrievable retrievable) {
auto& cache = SharedImmutableStringsCache::getSingleton();
auto uniqueChars = SourceTypeTraits<Unit>::toCacheable(std::move(source));
auto deduped = cache.getOrCreate(std::move(uniqueChars), length);
if (!deduped) {
ReportOutOfMemory(cx);
return false;
}
if (retrievable == SourceRetrievable::Yes) {
data = SourceType(
Uncompressed<Unit, SourceRetrievable::Yes>(std::move(deduped)));
}
else {
data = SourceType(
Uncompressed<Unit, SourceRetrievable::No>(std::move(deduped)));
}
return true;
}
template <
typename Unit>
[[nodiscard]]
bool ScriptSource::setRetrievedSource(JSContext* cx,
EntryUnits<Unit>&& source,
size_t length) {
MOZ_ASSERT(data.is<Retrievable<Unit>>(),
"retrieved source can only overwrite the corresponding "
"retrievable source");
return setUncompressedSourceHelper(cx, std::move(source), length,
SourceRetrievable::Yes);
}
bool js::IsOffThreadSourceCompressionEnabled() {
// If we don't have concurrent execution compression will contend with
// main-thread execution, in which case we disable. Similarly we don't want to
// block the thread pool if it is too small.
return GetHelperThreadCPUCount() > 1 && GetHelperThreadCount() > 1 &&
CanUseExtraThreads();
}
bool ScriptSource::tryCompressOffThread(JSContext* cx) {
// Beware: |js::SynchronouslyCompressSource| assumes that this function is
// only called once, just after a script has been compiled, and it's never
// called at some random time after that. If multiple calls of this can ever
// occur, that function may require changes.
// The SourceCompressionTask needs to record the major GC number for
// scheduling.
MOZ_ASSERT(CurrentThreadCanAccessRuntime(cx->runtime()));
// If source compression was already attempted, do not queue a new task.
if (hadCompressionTask_) {
return true;
}
if (!hasUncompressedSource()) {
// This excludes compressed, missing, and retrievable source.
return true;
}
// There are several cases where source compression is not a good idea:
// - If the script is tiny, then compression will save little or no space.
// - If there is only one core, then compression will contend with JS
// execution (which hurts benchmarketing).
//
// Otherwise, enqueue a compression task to be processed when a major
// GC is requested.
if (length() < ScriptSource::MinimumCompressibleLength ||
!IsOffThreadSourceCompressionEnabled()) {
return true;
}
// Heap allocate the task. It will be freed upon compression
// completing in AttachFinishedCompressedSources.
auto task = MakeUnique<SourceCompressionTask>(cx->runtime(),
this);
if (!task) {
ReportOutOfMemory(cx);
return false;
}
return EnqueueOffThreadCompression(cx, std::move(task));
}
template <
typename Unit>
void ScriptSource::triggerConvertToCompressedSource(
SharedImmutableString compressed, size_t uncompressedLength) {
MOZ_ASSERT(isUncompressed<Unit>(),
"should only be triggering compressed source installation to "
"overwrite identically-encoded uncompressed source");
MOZ_ASSERT(uncompressedData<Unit>()->length() == uncompressedLength);
// If units aren't pinned -- and they probably won't be, we'd have to have a
// GC in the small window of time where a |PinnedUnits| was live -- then we
// can immediately convert.
{
auto guard = readers_.lock();
if (MOZ_LIKELY(!guard->count)) {
convertToCompressedSource<Unit>(std::move(compressed),
uncompressedLength);
return;
}
// Otherwise, set aside the compressed-data info. The conversion is
// performed when the last |PinnedUnits| dies.
MOZ_ASSERT(guard->pendingCompressed.empty(),
"shouldn't be multiple conversions happening");
guard->pendingCompressed.construct<CompressedData<Unit>>(
std::move(compressed), uncompressedLength);
}
}
template <
typename Unit>
[[nodiscard]]
bool ScriptSource::initializeWithUnretrievableCompressedSource(
FrontendContext* fc, UniqueChars&& compressed, size_t rawLength,
size_t sourceLength) {
MOZ_ASSERT(data.is<Missing>(),
"shouldn't be double-initializing");
MOZ_ASSERT(compressed != nullptr);
auto& cache = SharedImmutableStringsCache::getSingleton();
auto deduped = cache.getOrCreate(std::move(compressed), rawLength);
if (!deduped) {
ReportOutOfMemory(fc);
return false;
}
#ifdef DEBUG
{
auto guard = readers_.lock();
MOZ_ASSERT(
guard->count == 0,
"shouldn't be initializing a ScriptSource while its characters "
"are pinned -- that only makes sense with a ScriptSource actively "
"being inspected");
}
#endif
data = SourceType(Compressed<Unit, SourceRetrievable::No>(std::move(deduped),
sourceLength));
return true;
}
template bool ScriptSource::initializeWithUnretrievableCompressedSource<
Utf8Unit>(FrontendContext* fc, UniqueChars&& compressed, size_t rawLength,
size_t sourceLength);
template bool ScriptSource::initializeWithUnretrievableCompressedSource<
char16_t>(FrontendContext* fc, UniqueChars&& compressed, size_t rawLength,
size_t sourceLength);
template <
typename Unit>
bool ScriptSource::assignSource(FrontendContext* fc,
const ReadOnlyCompileOptions& options,
SourceText<Unit>& srcBuf) {
MOZ_ASSERT(data.is<Missing>(),
"source assignment should only occur on fresh ScriptSources");
mutedErrors_ = options.mutedErrors();
delazificationMode_ = options.eagerDelazificationStrategy();
if (options.discardSource) {
return true;
}
if (options.sourceIsLazy) {
data = SourceType(Retrievable<Unit>());
return true;
}
auto& cache = SharedImmutableStringsCache::getSingleton();
auto deduped = cache.getOrCreate(srcBuf.get(), srcBuf.length(), [&srcBuf]() {
using CharT =
typename SourceTypeTraits<Unit>::CharT;
return srcBuf.ownsUnits()
? UniquePtr<CharT[], JS::FreePolicy>(srcBuf.takeChars())
: DuplicateString(srcBuf.get(), srcBuf.length());
});
if (!deduped) {
ReportOutOfMemory(fc);
return false;
}
data =
SourceType(Uncompressed<Unit, SourceRetrievable::No>(std::move(deduped)));
return true;
}
template bool ScriptSource::assignSource(FrontendContext* fc,
const ReadOnlyCompileOptions& options,
SourceText<char16_t>& srcBuf);
template bool ScriptSource::assignSource(FrontendContext* fc,
const ReadOnlyCompileOptions& options,
SourceText<Utf8Unit>& srcBuf);
[[nodiscard]]
static bool reallocUniquePtr(UniqueChars& unique, size_t size) {
auto newPtr =
static_cast<
char*>(js_realloc(unique.get(), size));
if (!newPtr) {
return false;
}
// Since the realloc succeeded, unique is now holding a freed pointer.
(
void)unique.release();
unique.reset(newPtr);
return true;
}
template <
typename Unit>
void SourceCompressionTask::workEncodingSpecific() {
MOZ_ASSERT(source_->isUncompressed<Unit>());
// Try to keep the maximum memory usage down by only allocating half the
// size of the string, first.
size_t inputBytes = source_->length() *
sizeof(Unit);
size_t firstSize = inputBytes / 2;
UniqueChars compressed(js_pod_malloc<
char>(firstSize));
if (!compressed) {
return;
}
const Unit* chars = source_->uncompressedData<Unit>()->units();
Compressor comp(
reinterpret_cast<
const unsigned char*>(chars), inputBytes);
if (!comp.init()) {
return;
}
comp.setOutput(
reinterpret_cast<
unsigned char*>(compressed.get()), firstSize);
bool cont =
true;
bool reallocated =
false;
while (cont) {
if (shouldCancel()) {
return;
}
switch (comp.compressMore()) {
case Compressor::
CONTINUE:
break;
case Compressor::MOREOUTPUT: {
if (reallocated) {
// The compressed string is longer than the original string.
return;
}
// The compressed output is greater than half the size of the
// original string. Reallocate to the full size.
if (!reallocUniquePtr(compressed, inputBytes)) {
return;
}
comp.setOutput(
reinterpret_cast<
unsigned char*>(compressed.get()),
inputBytes);
reallocated =
true;
break;
}
case Compressor::DONE:
cont =
false;
break;
case Compressor::OOM:
return;
}
}
size_t totalBytes = comp.totalBytesNeeded();
// Shrink the buffer to the size of the compressed data.
if (!reallocUniquePtr(compressed, totalBytes)) {
return;
}
comp.finish(compressed.get(), totalBytes);
if (shouldCancel()) {
return;
}
auto& strings = SharedImmutableStringsCache::getSingleton();
resultString_ = strings.getOrCreate(std::move(compressed), totalBytes);
}
struct SourceCompressionTask::PerformTaskWork {
SourceCompressionTask*
const task_;
explicit PerformTaskWork(SourceCompressionTask* task) : task_(task) {}
template <
typename Unit, SourceRetrievable CanRetrieve>
void operator()(
const ScriptSource::Uncompressed<Unit, CanRetrieve>&) {
task_->workEncodingSpecific<Unit>();
}
template <
typename T>
void operator()(
const T&) {
MOZ_CRASH(
"why are we compressing missing, missing-but-retrievable, "
"or already-compressed source?");
}
};
void ScriptSource::performTaskWork(SourceCompressionTask* task) {
MOZ_ASSERT(hasUncompressedSource());
data.match(SourceCompressionTask::PerformTaskWork(task));
}
void SourceCompressionTask::runTask() {
if (shouldCancel()) {
return;
}
MOZ_ASSERT(source_->hasUncompressedSource());
source_->performTaskWork(
this);
}
void SourceCompressionTask::runHelperThreadTask(
AutoLockHelperThreadState& locked) {
{
AutoUnlockHelperThreadState unlock(locked);
this->runTask();
}
{
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!HelperThreadState().compressionFinishedList(locked).append(
this)) {
oomUnsafe.crash(
"SourceCompressionTask::runHelperThreadTask");
}
}
}
void ScriptSource::triggerConvertToCompressedSourceFromTask(
SharedImmutableString compressed) {
data.match(TriggerConvertToCompressedSourceFromTask(
this, compressed));
}
void SourceCompressionTask::complete() {
if (!shouldCancel() && resultString_) {
source_->triggerConvertToCompressedSourceFromTask(std::move(resultString_));
}
}
bool js::SynchronouslyCompressSource(JSContext* cx,
JS::Handle<BaseScript*> script) {
// Finish all pending source compressions, including the single compression
// task that may have been created (by |ScriptSource::tryCompressOffThread|)
// just after the script was compiled. Because we have flushed this queue,
// no code below needs to synchronize with an off-thread parse task that
// assumes the immutability of a |ScriptSource|'s data.
//
// This *may* end up compressing |script|'s source. If it does -- we test
// this below -- that takes care of things. But if it doesn't, we will
// synchronously compress ourselves (and as noted above, this won't race
// anything).
RunPendingSourceCompressions(cx->runtime());
ScriptSource* ss = script->scriptSource();
#ifdef DEBUG
{
auto guard = ss->readers_.lock();
MOZ_ASSERT(guard->count == 0,
"can't synchronously compress while source units are in use");
}
#endif
// In principle a previously-triggered compression on a helper thread could
// have already completed. If that happens, there's nothing more to do.
if (ss->hasCompressedSource()) {
return true;
}
MOZ_ASSERT(ss->hasUncompressedSource(),
"shouldn't be compressing uncompressible source");
// Use an explicit scope to delineate the lifetime of |task|, for simplicity.
{
#ifdef DEBUG
uint32_t sourceRefs = ss->refs;
#endif
MOZ_ASSERT(sourceRefs > 0,
"at least |script| here should have a ref");
// |SourceCompressionTask::shouldCancel| can periodically result in source
// compression being canceled if we're not careful. Guarantee that two refs
// to |ss| are always live in this function (at least one preexisting and
// one held by the task) so that compression is never canceled.
auto task = MakeUnique<SourceCompressionTask>(cx->runtime(), ss);
if (!task) {
ReportOutOfMemory(cx);
return false;
}
MOZ_ASSERT(ss->refs > sourceRefs,
"must have at least two refs now");
// Attempt to compress. This may not succeed if OOM happens, but (because
// it ordinarily happens on a helper thread) no error will ever be set here.
MOZ_ASSERT(!cx->isExceptionPending());
ss->performTaskWork(task.get());
MOZ_ASSERT(!cx->isExceptionPending());
// Convert |ss| from uncompressed to compressed data.
task->complete();
MOZ_ASSERT(!cx->isExceptionPending());
}
// The only way source won't be compressed here is if OOM happened.
return ss->hasCompressedSource();
}
void ScriptSource::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
JS::ScriptSourceInfo* info)
const {
info->misc += mallocSizeOf(
this);
info->numScripts++;
}
frontend::InitialStencilAndDelazifications*
ScriptSourceObject::maybeGetStencils() {
Value stencilsVal = getReservedSlot(STENCILS_SLOT);
if (stencilsVal.isUndefined()) {
return nullptr;
}
return reinterpret_cast<frontend::InitialStencilAndDelazifications*>(
uintptr_t(stencilsVal.toPrivate()) & ~STENCILS_MASK);
}
void ScriptSourceObject::clearStencils() {
auto* stencils = maybeGetStencils();
if (!stencils) {
return;
}
stencils->Release();
setReservedSlot(STENCILS_SLOT, UndefinedValue());
}
template <uintptr_t flag>
void ScriptSourceObject::setStencilsFlag() {
JS::Value stencilsVal = getReservedSlot(STENCILS_SLOT);
MOZ_ASSERT(!stencilsVal.isUndefined(),
"This should be called after setStencils");
uintptr_t raw = uintptr_t(stencilsVal.toPrivate());
MOZ_ASSERT((raw & flag) == 0);
raw |= flag;
setReservedSlot(STENCILS_SLOT, PrivateValue(raw));
}
template <uintptr_t flag>
void ScriptSourceObject::unsetStencilsFlag() {
JS::Value stencilsVal = getReservedSlot(STENCILS_SLOT);
MOZ_ASSERT(!stencilsVal.isUndefined(),
"This should be called after setStencils");
uintptr_t raw = uintptr_t(stencilsVal.toPrivate());
raw &= ~flag;
if (raw & STENCILS_MASK) {
setReservedSlot(STENCILS_SLOT, PrivateValue(raw));
}
else {
clearStencils();
}
}
template <uintptr_t flag>
bool ScriptSourceObject::isStencilsFlagSet()
const {
JS::Value stencilsVal = getReservedSlot(STENCILS_SLOT);
if (stencilsVal.isUndefined()) {
return false;
}
uintptr_t raw = uintptr_t(stencilsVal.toPrivate());
return bool(raw & flag);
}
void ScriptSourceObject::setStencils(
already_AddRefed<frontend::InitialStencilAndDelazifications> stencils) {
MOZ_ASSERT(!maybeGetStencils());
setReservedSlot(STENCILS_SLOT, PrivateValue(stencils.take()));
}
void ScriptSourceObject::setCollectingDelazifications() {
setStencilsFlag<STENCILS_COLLECTING_DELAZIFICATIONS_FLAG>();
}
void ScriptSourceObject::unsetCollectingDelazifications() {
unsetStencilsFlag<STENCILS_COLLECTING_DELAZIFICATIONS_FLAG>();
}
bool ScriptSourceObject::isCollectingDelazifications()
const {
return isStencilsFlagSet<STENCILS_COLLECTING_DELAZIFICATIONS_FLAG>();
}
void ScriptSourceObject::setSharingDelazifications() {
setStencilsFlag<STENCILS_SHARING_DELAZIFICATIONS_FLAG>();
}
bool ScriptSourceObject::isSharingDelazifications()
const {
return isStencilsFlagSet<STENCILS_SHARING_DELAZIFICATIONS_FLAG>();
}
template <
typename Unit>
[[nodiscard]]
bool ScriptSource::initializeUnretrievableUncompressedSource(
FrontendContext* fc, EntryUnits<Unit>&& source, size_t length) {
MOZ_ASSERT(data.is<Missing>(),
"must be initializing a fresh ScriptSource");
return setUncompressedSourceHelper(fc, std::move(source), length,
SourceRetrievable::No);
}
template bool ScriptSource::initializeUnretrievableUncompressedSource(
FrontendContext* fc, EntryUnits<Utf8Unit>&& source, size_t length);
template bool ScriptSource::initializeUnretrievableUncompressedSource(
FrontendContext* fc, EntryUnits<char16_t>&& source, size_t length);
// Format and return a cx->pod_malloc'ed URL for a generated script like:
// {filename} line {lineno} > {introducer}
// For example:
// foo.js line 7 > eval
// indicating code compiled by the call to 'eval' on line 7 of foo.js.
UniqueChars js::FormatIntroducedFilename(
const char* filename, uint32_t lineno,
const char* introducer) {
// Compute the length of the string in advance, so we can allocate a
// buffer of the right size on the first shot.
//
// (JS_smprintf would be perfect, as that allocates the result
// dynamically as it formats the string, but it won't allocate from cx,
// and wants us to use a special free function.)
char linenoBuf[15];
size_t filenameLen = strlen(filename);
size_t linenoLen = SprintfLiteral(linenoBuf,
"%u", lineno);
size_t introducerLen = strlen(introducer);
size_t len = filenameLen + 6
/* == strlen(" line ") */ + linenoLen +
3
/* == strlen(" > ") */ + introducerLen + 1 /* \0 */;
UniqueChars formatted(js_pod_malloc<
char>(len));
if (!formatted) {
return nullptr;
}
mozilla::DebugOnly<size_t> checkLen = snprintf(
formatted.get(), len,
"%s line %s > %s", filename, linenoBuf, introducer);
MOZ_ASSERT(checkLen == len - 1);
return formatted;
}
bool ScriptSource::initFromOptions(FrontendContext* fc,
const ReadOnlyCompileOptions& options) {
MOZ_ASSERT(!filename_);
MOZ_ASSERT(!introducerFilename_);
mutedErrors_ = options.mutedErrors();
delazificationMode_ = options.eagerDelazificationStrategy();
startLine_ = options.lineno;
startColumn_ = JS::LimitedColumnNumberOneOrigin::fromUnlimited(
JS::ColumnNumberOneOrigin(options.column));
introductionType_ = options.introductionType;
setIntroductionOffset(options.introductionOffset);
// The parameterListEnd_ is initialized later by setParameterListEnd, before
// we expose any scripts that use this ScriptSource to the debugger.
if (options.hasIntroductionInfo) {
MOZ_ASSERT(options.introductionType != nullptr);
const char* filename =
options.filename() ? options.filename().c_str() :
"";
UniqueChars formatted = FormatIntroducedFilename(
filename, options.introductionLineno, options.introductionType);
if (!formatted) {
ReportOutOfMemory(fc);
return false;
}
if (!setFilename(fc, std::move(formatted))) {
return false;
}
}
else if (options.filename()) {
if (!setFilename(fc, options.filename().c_str())) {
return false;
}
}
if (options.introducerFilename()) {
if (!setIntroducerFilename(fc, options.introducerFilename().c_str())) {
return false;
}
}
return true;
}
// Use the SharedImmutableString map to deduplicate input string. The input
// string must be null-terminated.
template <
typename SharedT,
typename CharT>
static SharedT GetOrCreateStringZ(FrontendContext* fc,
UniquePtr<CharT[], JS::FreePolicy>&& str) {
size_t lengthWithNull = std::char_traits<CharT>::length(str.get()) + 1;
auto res = SharedImmutableStringsCache::getSingleton().getOrCreate(
std::move(str), lengthWithNull);
if (!res) {
ReportOutOfMemory(fc);
}
return res;
}
SharedImmutableString ScriptSource::getOrCreateStringZ(FrontendContext* fc,
UniqueChars&& str) {
return GetOrCreateStringZ<SharedImmutableString>(fc, std::move(str));
}
SharedImmutableTwoByteString ScriptSource::getOrCreateStringZ(
FrontendContext* fc, UniqueTwoByteChars&& str) {
return GetOrCreateStringZ<SharedImmutableTwoByteString>(fc, std::move(str));
}
bool ScriptSource::setFilename(FrontendContext* fc,
const char* filename) {
UniqueChars owned = DuplicateString(fc, filename);
if (!owned) {
return false;
}
return setFilename(fc, std::move(owned));
}
bool ScriptSource::setFilename(FrontendContext* fc, UniqueChars&& filename) {
MOZ_ASSERT(!filename_);
filename_ = getOrCreateStringZ(fc, std::move(filename));
if (filename_) {
filenameHash_ =
mozilla::HashStringKnownLength(filename_.chars(), filename_.length());
return true;
}
return false;
}
bool ScriptSource::setIntroducerFilename(FrontendContext* fc,
const char* filename) {
UniqueChars owned = DuplicateString(fc, filename);
if (!owned) {
return false;
}
return setIntroducerFilename(fc, std::move(owned));
}
bool ScriptSource::setIntroducerFilename(FrontendContext* fc,
UniqueChars&& filename) {
MOZ_ASSERT(!introducerFilename_);
introducerFilename_ = getOrCreateStringZ(fc, std::move(filename));
return bool(introducerFilename_);
}
bool ScriptSource::setDisplayURL(FrontendContext* fc,
const char16_t* url) {
UniqueTwoByteChars owned = DuplicateString(fc, url);
if (!owned) {
return false;
}
return setDisplayURL(fc, std::move(owned));
}
bool ScriptSource::setDisplayURL(FrontendContext* fc,
UniqueTwoByteChars&& url) {
MOZ_ASSERT(!hasDisplayURL());
MOZ_ASSERT(url);
if (url[0] ==
'\0') {
return true;
}
displayURL_ = getOrCreateStringZ(fc, std::move(url));
return bool(displayURL_);
}
bool ScriptSource::setSourceMapURL(FrontendContext* fc,
const char16_t* url) {
UniqueTwoByteChars owned = DuplicateString(fc, url);
if (!owned) {
return false;
}
return setSourceMapURL(fc, std::move(owned));
}
bool ScriptSource::setSourceMapURL(FrontendContext* fc,
UniqueTwoByteChars&& url) {
MOZ_ASSERT(url);
if (url[0] ==
'\0') {
return true;
}
sourceMapURL_ = getOrCreateStringZ(fc, std::move(url));
return bool(sourceMapURL_);
}
/* static */ mozilla::Atomic<uint32_t, mozilla::SequentiallyConsistent>
ScriptSource::idCount_;
/*
* [SMDOC] JSScript data layout (immutable)
*
* Script data that shareable across processes. There are no pointers (GC or
* otherwise) and the data is relocatable.
*
* Array elements Pointed to by Length
* -------------- ------------- ------
* jsbytecode code() codeLength()
* jsscrnote notes() noteLength()
* uint32_t resumeOffsets()
* ScopeNote scopeNotes()
* TryNote tryNotes()
*/
/* static */ CheckedInt<uint32_t> ImmutableScriptData::sizeFor(
--> --------------------
--> maximum size reached
--> --------------------