diff --git a/JSTests/controlFlowProfiler/function-names.js b/JSTests/controlFlowProfiler/function-names.js new file mode 100644 index 000000000000..8af099523f16 --- /dev/null +++ b/JSTests/controlFlowProfiler/function-names.js @@ -0,0 +1,44 @@ +var getFunctionRanges = $vm.getFunctionRanges; + +load("./driver/driver.js"); + +function namedFunction() { return 1; } +function anotherFunction() { return 2; } +var arrowFunc = () => 3; + +// Only call namedFunction to test executed vs not-executed. +namedFunction(); + +var ranges = getFunctionRanges(namedFunction); + +// We should have at least the functions declared in this source. +assert(ranges.length >= 3, "Expected at least 3 function ranges, got " + ranges.length); + +// Find our named functions in the ranges. +var namedFunctionRange = null; +var anotherFunctionRange = null; +var arrowFuncRange = null; + +for (var i = 0; i < ranges.length; i++) { + if (ranges[i].name === "namedFunction") + namedFunctionRange = ranges[i]; + else if (ranges[i].name === "anotherFunction") + anotherFunctionRange = ranges[i]; + else if (ranges[i].name === "arrowFunc") + arrowFuncRange = ranges[i]; +} + +// Verify named functions have their names. +assert(namedFunctionRange !== null, "Should find 'namedFunction' in ranges"); +assert(anotherFunctionRange !== null, "Should find 'anotherFunction' in ranges"); +assert(arrowFuncRange !== null, "Should find 'arrowFunc' in ranges"); + +// Verify execution status. +assert(namedFunctionRange.hasExecuted === true, "namedFunction should have executed"); +assert(anotherFunctionRange.hasExecuted === false, "anotherFunction should not have executed"); +assert(arrowFuncRange.hasExecuted === false, "arrowFunc should not have executed"); + +// Verify ranges are valid (start < end). +assert(namedFunctionRange.start < namedFunctionRange.end, "namedFunction range should be valid"); +assert(anotherFunctionRange.start < anotherFunctionRange.end, "anotherFunction range should be valid"); +assert(arrowFuncRange.start < arrowFuncRange.end, "arrowFunc range should be valid"); diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index 35c8be16a1a0..b16a2edb4810 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -434,7 +434,7 @@ bool CodeBlock::finishCreation(VM& vm, ScriptExecutable* ownerExecutable, Unlink for (size_t count = unlinkedCodeBlock->numberOfFunctionDecls(), i = 0; i < count; ++i) { UnlinkedFunctionExecutable* unlinkedExecutable = unlinkedCodeBlock->functionDecl(i); if (shouldUpdateFunctionHasExecutedCache) - vm.functionHasExecutedCache()->insertUnexecutedRange(ownerExecutable->sourceID(), unlinkedExecutable->unlinkedFunctionStart(), unlinkedExecutable->unlinkedFunctionEnd()); + vm.functionHasExecutedCache()->insertUnexecutedRange(ownerExecutable->sourceID(), unlinkedExecutable->unlinkedFunctionStart(), unlinkedExecutable->unlinkedFunctionEnd(), unlinkedExecutable->ecmaName().string()); m_functionDecls[i].set(vm, this, unlinkedExecutable->link(vm, topLevelExecutable, ownerExecutable->source(), std::nullopt, NoIntrinsic, ownerExecutable->isInsideOrdinaryFunction())); } @@ -442,7 +442,7 @@ bool CodeBlock::finishCreation(VM& vm, ScriptExecutable* ownerExecutable, Unlink for (size_t count = unlinkedCodeBlock->numberOfFunctionExprs(), i = 0; i < count; ++i) { UnlinkedFunctionExecutable* unlinkedExecutable = unlinkedCodeBlock->functionExpr(i); if (shouldUpdateFunctionHasExecutedCache) - vm.functionHasExecutedCache()->insertUnexecutedRange(ownerExecutable->sourceID(), unlinkedExecutable->unlinkedFunctionStart(), unlinkedExecutable->unlinkedFunctionEnd()); + vm.functionHasExecutedCache()->insertUnexecutedRange(ownerExecutable->sourceID(), unlinkedExecutable->unlinkedFunctionStart(), unlinkedExecutable->unlinkedFunctionEnd(), unlinkedExecutable->ecmaName().string()); m_functionExprs[i].set(vm, this, unlinkedExecutable->link(vm, topLevelExecutable, ownerExecutable->source(), std::nullopt, NoIntrinsic, ownerExecutable->isInsideOrdinaryFunction())); } diff --git a/Source/JavaScriptCore/runtime/ControlFlowProfiler.cpp b/Source/JavaScriptCore/runtime/ControlFlowProfiler.cpp index 1a13e46d1e3b..4662e831b531 100644 --- a/Source/JavaScriptCore/runtime/ControlFlowProfiler.cpp +++ b/Source/JavaScriptCore/runtime/ControlFlowProfiler.cpp @@ -118,7 +118,7 @@ Vector ControlFlowProfiler::getBasicBlocksForSourceID(SourceID } } - const Vector>& functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID); + const auto functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID); for (const auto& functionRange : functionRanges) { BasicBlockRange range; range.m_hasExecuted = std::get<0>(functionRange); diff --git a/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.cpp b/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.cpp index 1988ef89fbbe..74053968d71c 100644 --- a/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.cpp +++ b/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.cpp @@ -42,7 +42,7 @@ bool FunctionHasExecutedCache::hasExecutedAtOffset(SourceID id, unsigned offset) for (auto& pair : map) { const FunctionRange& range = pair.key.key(); if (range.m_start <= offset && offset <= range.m_end && range.m_end - range.m_start < distance) { - hasExecuted = pair.value; + hasExecuted = pair.value.m_hasExecuted; distance = range.m_end - range.m_start; } } @@ -50,7 +50,7 @@ bool FunctionHasExecutedCache::hasExecutedAtOffset(SourceID id, unsigned offset) return hasExecuted; } -void FunctionHasExecutedCache::insertUnexecutedRange(SourceID id, unsigned start, unsigned end) +void FunctionHasExecutedCache::insertUnexecutedRange(SourceID id, unsigned start, unsigned end, const String& functionName) { RangeMap& map = m_rangeMap.add(id, RangeMap { }).iterator->value; FunctionRange range; @@ -58,7 +58,7 @@ void FunctionHasExecutedCache::insertUnexecutedRange(SourceID id, unsigned start range.m_end = end; // Only insert unexecuted ranges once for a given sourceID because we may run into a situation where an executable executes, then is GCed, and then is allocated again, // and tries to reinsert itself, claiming it has never run, but this is false because it indeed already executed. - map.add(range, false); + map.add(range, RangeValue { false, functionName }); } void FunctionHasExecutedCache::removeUnexecutedRange(SourceID id, unsigned start, unsigned end) @@ -73,12 +73,16 @@ void FunctionHasExecutedCache::removeUnexecutedRange(SourceID id, unsigned start FunctionRange range; range.m_start = start; range.m_end = end; - map.set(range, true); + auto existingIt = map.find(range); + if (existingIt != map.end()) + existingIt->value.m_hasExecuted = true; + else + map.set(range, RangeValue { true, String() }); } -Vector> FunctionHasExecutedCache::getFunctionRanges(SourceID id) +Vector> FunctionHasExecutedCache::getFunctionRanges(SourceID id) { - Vector> ranges(0); + Vector> ranges(0); auto iterator = m_rangeMap.find(id); if (iterator == m_rangeMap.end()) return ranges; @@ -86,8 +90,8 @@ Vector> FunctionHasExecutedCache::getFuncti RangeMap& map = iterator->value; for (auto& pair : map) { const FunctionRange& range = pair.key.key(); - bool hasExecuted = pair.value; - ranges.append(std::tuple(hasExecuted, range.m_start, range.m_end)); + const RangeValue& value = pair.value; + ranges.append(std::make_tuple(value.m_hasExecuted, range.m_start, range.m_end, value.m_functionName)); } return ranges; diff --git a/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.h b/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.h index 805faa2fac8b..4582b6d6dc47 100644 --- a/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.h +++ b/Source/JavaScriptCore/runtime/FunctionHasExecutedCache.h @@ -30,6 +30,7 @@ #include #include #include +#include namespace JSC { @@ -47,13 +48,18 @@ class FunctionHasExecutedCache { unsigned m_end; }; + struct RangeValue { + bool m_hasExecuted { false }; + String m_functionName; + }; + bool hasExecutedAtOffset(SourceID, unsigned offset); - void insertUnexecutedRange(SourceID, unsigned start, unsigned end); + void insertUnexecutedRange(SourceID, unsigned start, unsigned end, const String& functionName = String()); void removeUnexecutedRange(SourceID, unsigned start, unsigned end); - Vector> getFunctionRanges(SourceID); + Vector> getFunctionRanges(SourceID); private: - using RangeMap = UncheckedKeyHashMap, bool>; + using RangeMap = UncheckedKeyHashMap, RangeValue>; using SourceIDToRangeMap = UncheckedKeyHashMap, RangeMap>; SourceIDToRangeMap m_rangeMap; }; diff --git a/Source/JavaScriptCore/runtime/JSModuleRecord.cpp b/Source/JavaScriptCore/runtime/JSModuleRecord.cpp index 24cea00a3f69..bb395d9d24c4 100644 --- a/Source/JavaScriptCore/runtime/JSModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleRecord.cpp @@ -283,7 +283,8 @@ void JSModuleRecord::instantiateDeclarations(JSGlobalObject* globalObject, Modul if (vm.typeProfiler() || vm.controlFlowProfiler()) { vm.functionHasExecutedCache()->insertUnexecutedRange(moduleProgramExecutable->sourceID(), unlinkedFunctionExecutable->unlinkedFunctionStart(), - unlinkedFunctionExecutable->unlinkedFunctionEnd()); + unlinkedFunctionExecutable->unlinkedFunctionEnd(), + unlinkedFunctionExecutable->ecmaName().string()); } auto* executable = unlinkedFunctionExecutable->link(vm, moduleProgramExecutable, moduleProgramExecutable->source()); SourceParseMode parseMode = executable->parseMode(); diff --git a/Source/JavaScriptCore/runtime/ProgramExecutable.cpp b/Source/JavaScriptCore/runtime/ProgramExecutable.cpp index 1fbc30580398..5c02ffb43859 100644 --- a/Source/JavaScriptCore/runtime/ProgramExecutable.cpp +++ b/Source/JavaScriptCore/runtime/ProgramExecutable.cpp @@ -230,9 +230,10 @@ JSObject* ProgramExecutable::initializeGlobalProperties(VM& vm, JSGlobalObject* globalObject->createGlobalFunctionBinding(unlinkedFunctionExecutable->name()); RETURN_IF_EXCEPTION(throwScope, nullptr); if (vm.typeProfiler() || vm.controlFlowProfiler()) { - vm.functionHasExecutedCache()->insertUnexecutedRange(sourceID(), + vm.functionHasExecutedCache()->insertUnexecutedRange(sourceID(), unlinkedFunctionExecutable->unlinkedFunctionStart(), - unlinkedFunctionExecutable->unlinkedFunctionEnd()); + unlinkedFunctionExecutable->unlinkedFunctionEnd(), + unlinkedFunctionExecutable->ecmaName().string()); } } diff --git a/Source/JavaScriptCore/tools/JSDollarVM.cpp b/Source/JavaScriptCore/tools/JSDollarVM.cpp index b2e73561ad48..5b4367fd830b 100644 --- a/Source/JavaScriptCore/tools/JSDollarVM.cpp +++ b/Source/JavaScriptCore/tools/JSDollarVM.cpp @@ -2188,6 +2188,7 @@ static JSC_DECLARE_HOST_FUNCTION(functionFlattenDictionaryObject); static JSC_DECLARE_HOST_FUNCTION(functionDumpBasicBlockExecutionRanges); static JSC_DECLARE_HOST_FUNCTION(functionHasBasicBlockExecuted); static JSC_DECLARE_HOST_FUNCTION(functionBasicBlockExecutionCount); +static JSC_DECLARE_HOST_FUNCTION(functionGetFunctionRanges); static JSC_DECLARE_HOST_FUNCTION(functionEnableDebuggerModeWhenIdle); static JSC_DECLARE_HOST_FUNCTION(functionDisableDebuggerModeWhenIdle); static JSC_DECLARE_HOST_FUNCTION(functionDeleteAllCodeWhenIdle); @@ -3654,6 +3655,44 @@ JSC_DEFINE_HOST_FUNCTION(functionBasicBlockExecutionCount, (JSGlobalObject* glob return JSValue::encode(JSValue(executionCount)); } +// $vm.getFunctionRanges(sourceFunction) returns an array of { hasExecuted, start, end, name } +// for all functions declared in the same source as the given function. +JSC_DEFINE_HOST_FUNCTION(functionGetFunctionRanges, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + DollarVMAssertScope assertScope; + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue functionValue = callFrame->argument(0); + if (!functionValue.isCallable()) + return throwVMTypeError(globalObject, scope, "Expected argument to be callable"_s); + JSFunction* function = jsDynamicCast(functionValue); + if (!function) + return throwVMTypeError(globalObject, scope, "Expected argument to be a JSFunction"_s); + FunctionExecutable* executable = jsDynamicCast(function->executable()); + if (!executable) + return throwVMTypeError(globalObject, scope, "Expected argument to be a non-host JSFunction"_s); + + const auto functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(executable->sourceID()); + + JSArray* result = constructEmptyArray(globalObject, nullptr, functionRanges.size()); + RETURN_IF_EXCEPTION(scope, encodedJSUndefined()); + + Structure* entryStructure = JSFinalObject::createStructure(vm, globalObject, globalObject->objectPrototype(), 4); + for (size_t i = 0; i < functionRanges.size(); i++) { + JSObject* entry = JSFinalObject::create(vm, entryStructure); + entry->putDirect(vm, Identifier::fromString(vm, "hasExecuted"_s), jsBoolean(std::get<0>(functionRanges[i]))); + entry->putDirect(vm, Identifier::fromString(vm, "start"_s), jsNumber(std::get<1>(functionRanges[i]))); + entry->putDirect(vm, Identifier::fromString(vm, "end"_s), jsNumber(std::get<2>(functionRanges[i]))); + const String& name = std::get<3>(functionRanges[i]); + entry->putDirect(vm, Identifier::fromString(vm, "name"_s), name.isEmpty() ? jsEmptyString(vm) : jsString(vm, name)); + result->putDirectIndex(globalObject, i, entry); + RETURN_IF_EXCEPTION(scope, encodedJSUndefined()); + } + + return JSValue::encode(result); +} + class DoNothingDebugger final : public Debugger { WTF_MAKE_NONCOPYABLE(DoNothingDebugger); WTF_MAKE_TZONE_ALLOCATED(DoNothingDebugger); @@ -4438,6 +4477,7 @@ void JSDollarVM::finishCreation(VM& vm) addFunction(vm, "dumpBasicBlockExecutionRanges"_s, functionDumpBasicBlockExecutionRanges , 0); addFunction(vm, "hasBasicBlockExecuted"_s, functionHasBasicBlockExecuted, 2); addFunction(vm, "basicBlockExecutionCount"_s, functionBasicBlockExecutionCount, 2); + addFunction(vm, "getFunctionRanges"_s, functionGetFunctionRanges, 1); addFunction(vm, "enableDebuggerModeWhenIdle"_s, functionEnableDebuggerModeWhenIdle, 0); addFunction(vm, "disableDebuggerModeWhenIdle"_s, functionDisableDebuggerModeWhenIdle, 0);