lib/ConsoleHost/ConsoleHost.cpp (329 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/ConsoleHost/ConsoleHost.h" #include "hermes/CompilerDriver/CompilerDriver.h" #include "hermes/Support/MemoryBuffer.h" #include "hermes/Support/UTF8.h" #include "hermes/VM/Callable.h" #include "hermes/VM/Domain.h" #include "hermes/VM/JSObject.h" #include "hermes/VM/MockedEnvironment.h" #include "hermes/VM/NativeArgs.h" #include "hermes/VM/Profiler/SamplingProfiler.h" #include "hermes/VM/Runtime.h" #include "hermes/VM/StringPrimitive.h" #include "hermes/VM/StringView.h" #include "hermes/VM/TimeLimitMonitor.h" #include "hermes/VM/instrumentation/PerfEvents.h" namespace hermes { ConsoleHostContext::ConsoleHostContext(vm::Runtime &runtime) { runtime.addCustomRootsFunction([this](vm::GC *, vm::RootAcceptor &acceptor) { for (auto &entry : taskQueue_) { acceptor.acceptPtr(entry.second); } }); } /// Raises an uncatchable quit exception. static vm::CallResult<vm::HermesValue> quit(void *, vm::Runtime &runtime, vm::NativeArgs) { return runtime.raiseQuitError(); } static void printStats(vm::Runtime &runtime, llvh::raw_ostream &os) { std::string stats; { llvh::raw_string_ostream tmp{stats}; runtime.printHeapStats(tmp); } vm::instrumentation::PerfEvents::endAndInsertStats(stats); os << stats; } static vm::CallResult<vm::HermesValue> createHeapSnapshot(void *, vm::Runtime &runtime, vm::NativeArgs args) { using namespace vm; std::string fileName; if (args.getArgCount() >= 1 && !args.getArg(0).isUndefined()) { if (!args.getArg(0).isString()) { return runtime.raiseTypeError("Filename argument must be a string"); } auto str = Handle<StringPrimitive>::vmcast(args.getArgHandle(0)); auto jsFileName = StringPrimitive::createStringView(runtime, str); llvh::SmallVector<char16_t, 16> buf; convertUTF16ToUTF8WithReplacements(fileName, jsFileName.getUTF16Ref(buf)); } if (fileName.empty()) { // "-" is recognized as stdout. fileName = "-"; } else if ( !llvh::StringRef{fileName}.endswith(".heapsnapshot") && !llvh::StringRef{fileName}.endswith(".heaptimeline")) { return runtime.raiseTypeError( "Filename must end in .heapsnapshot or .heaptimeline"); } if (auto err = runtime.getHeap().createSnapshotToFile(fileName)) { // This isn't a TypeError, but no other built-in can express file errors, // so this will have to do. return runtime.raiseTypeError( TwineChar16("Could not write out to the file located at \"") + llvh::StringRef(fileName) + "\". System error: " + llvh::StringRef(err.message())); } return HermesValue::encodeUndefinedValue(); } static vm::CallResult<vm::HermesValue> loadSegment(void *ctx, vm::Runtime &runtime, vm::NativeArgs args) { using namespace hermes::vm; const auto *baseFilename = reinterpret_cast<std::string *>(ctx); auto requireContext = args.dyncastArg<RequireContext>(0); if (!requireContext) { return runtime.raiseTypeError( "First argument to loadSegment must be context"); } auto segmentRes = toUInt32_RJS(runtime, args.getArgHandle(1)); if (LLVM_UNLIKELY(segmentRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } uint32_t segment = segmentRes->getNumberAs<uint32_t>(); auto fileBufRes = llvh::MemoryBuffer::getFile(Twine(*baseFilename) + "." + Twine(segment)); if (!fileBufRes) { return runtime.raiseTypeError( TwineChar16("Failed to open segment: ") + segment); } auto ret = hbc::BCProviderFromBuffer::createBCProviderFromBuffer( std::make_unique<OwnedMemoryBuffer>(std::move(*fileBufRes))); if (!ret.first) { return runtime.raiseTypeError("Error deserializing bytecode"); } if (LLVM_UNLIKELY( runtime.loadSegment(std::move(ret.first), requireContext) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return HermesValue::encodeUndefinedValue(); } static vm::CallResult<vm::HermesValue> setTimeout(void *ctx, vm::Runtime &runtime, vm::NativeArgs args) { ConsoleHostContext *consoleHost = (ConsoleHostContext *)ctx; using namespace hermes::vm; Handle<Callable> callable = args.dyncastArg<Callable>(0); if (!callable) { return runtime.raiseTypeError("Argument to setTimeout must be a function"); } CallResult<HermesValue> boundFunction = BoundFunction::create( runtime, callable, args.getArgCount() - 1, args.begin() + 1); if (boundFunction == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; uint32_t taskId = consoleHost->queueTask( PseudoHandle<Callable>::vmcast(createPseudoHandle(*boundFunction))); return HermesValue::encodeNumberValue(taskId); } static vm::CallResult<vm::HermesValue> clearTimeout(void *ctx, vm::Runtime &runtime, vm::NativeArgs args) { ConsoleHostContext *consoleHost = (ConsoleHostContext *)ctx; using namespace hermes::vm; if (!args.getArg(0).isNumber()) { return runtime.raiseTypeError("Argument to clearTimeout must be a number"); } consoleHost->clearTask(args.getArg(0).getNumberAs<uint32_t>()); return HermesValue::encodeUndefinedValue(); } void installConsoleBindings( vm::Runtime &runtime, ConsoleHostContext &ctx, vm::StatSamplingThread *statSampler, const std::string *filename) { vm::DefinePropertyFlags normalDPF = vm::DefinePropertyFlags::getNewNonEnumerableFlags(); auto defineGlobalFunc = [&](vm::SymbolID name, vm::NativeFunctionPtr functionPtr, void *context, unsigned paramCount) -> void { vm::GCScopeMarkerRAII marker{runtime}; auto func = vm::NativeFunction::createWithoutPrototype( runtime, context, functionPtr, name, paramCount); auto res = vm::JSObject::defineOwnProperty( runtime.getGlobal(), runtime, name, normalDPF, func); (void)res; assert( res != vm::ExecutionStatus::EXCEPTION && *res && "global.defineOwnProperty() failed"); }; // Define the 'quit' function. defineGlobalFunc( vm::Predefined::getSymbolID(vm::Predefined::quit), quit, nullptr, 0); defineGlobalFunc( vm::Predefined::getSymbolID(vm::Predefined::createHeapSnapshot), createHeapSnapshot, nullptr, 1); // Define the 'loadSegment' function. defineGlobalFunc( runtime .ignoreAllocationFailure(runtime.getIdentifierTable().getSymbolHandle( runtime, llvh::createASCIIRef("loadSegment"))) .get(), loadSegment, reinterpret_cast<void *>(const_cast<std::string *>(filename)), 2); defineGlobalFunc( runtime .ignoreAllocationFailure(runtime.getIdentifierTable().getSymbolHandle( runtime, llvh::createASCIIRef("setTimeout"))) .get(), setTimeout, &ctx, 2); defineGlobalFunc( runtime .ignoreAllocationFailure(runtime.getIdentifierTable().getSymbolHandle( runtime, llvh::createASCIIRef("clearTimeout"))) .get(), clearTimeout, &ctx, 1); // Define `setImmediate` to be the same as `setTimeout` here. // `setTimeout` doesn't use the time provided to it, and due to this // being CLI code, we don't have an event loop. // This allows the Promise polyfill to work enough for testing in the // terminal, though other hosts should provide their own implementation of the // event loop. defineGlobalFunc( runtime .ignoreAllocationFailure(runtime.getIdentifierTable().getSymbolHandle( runtime, llvh::createASCIIRef("setImmediate"))) .get(), setTimeout, &ctx, 1); } // If a function body might throw C++ exceptions other than // jsi::JSError from Hermes, it should be wrapped in this form: // // return maybeCatchException([&] { body }) // // This will execute body; if exceptions are enabled, this execution // will be wrapped in a try/catch that catches those exceptions, report it then // exit. namespace { template <typename F> auto maybeCatchException(const F &f) -> decltype(f()) { #if defined(HERMESVM_EXCEPTION_ON_OOM) try { return f(); } catch (const std::exception &ex) { // Report thrown exception and exit the process with failure code. llvh::errs() << ex.what(); exit(1); } #else // HERMESVM_EXCEPTION_ON_OOM return f(); #endif } bool executeHBCBytecodeImpl( std::shared_ptr<hbc::BCProvider> &&bytecode, const ExecuteOptions &options, const std::string *filename) { bool shouldRecordGCStats = options.runtimeConfig.getGCConfig().getShouldRecordStats(); if (shouldRecordGCStats) { vm::instrumentation::PerfEvents::begin(); } std::unique_ptr<vm::StatSamplingThread> statSampler; auto runtime = vm::Runtime::create(options.runtimeConfig); if (options.stabilizeInstructionCount) { // Try to limit features that can introduce unpredictable CPU instruction // behavior. Date is a potential cause, but is not handled currently. vm::MockedEnvironment env; env.mathRandomSeed = 0; env.stabilizeInstructionCount = true; runtime->setMockedEnvironment(env); } if (options.timeLimit > 0) { vm::TimeLimitMonitor::getInstance().watchRuntime( *runtime, options.timeLimit); } if (shouldRecordGCStats) { statSampler = std::make_unique<vm::StatSamplingThread>( std::chrono::milliseconds(100)); } if (options.heapTimeline) { runtime->enableAllocationLocationTracker(); } vm::GCScope scope(*runtime); ConsoleHostContext ctx{*runtime}; installConsoleBindings(*runtime, ctx, statSampler.get(), filename); vm::RuntimeModuleFlags flags; flags.persistent = true; if (options.stopAfterInit) { vm::Handle<vm::Domain> domain = runtime->makeHandle(vm::Domain::create(*runtime)); if (LLVM_UNLIKELY( vm::RuntimeModule::create( *runtime, domain, facebook::hermes::debugger::kInvalidLocation, std::move(bytecode), flags) == vm::ExecutionStatus::EXCEPTION)) { llvh::errs() << "Failed to initialize main RuntimeModule\n"; return false; } return true; } if (options.sampleProfiling) { vm::SamplingProfiler::enable(); } llvh::StringRef sourceURL{}; if (filename) sourceURL = *filename; vm::CallResult<vm::HermesValue> status = runtime->runBytecode( std::move(bytecode), flags, sourceURL, vm::Runtime::makeNullHandle<vm::Environment>()); if (options.sampleProfiling) { vm::SamplingProfiler::disable(); vm::SamplingProfiler::dumpChromeTraceGlobal(llvh::errs()); } bool threwException = status == vm::ExecutionStatus::EXCEPTION; if (threwException) { // Make sure stdout catches up to stderr. llvh::outs().flush(); runtime->printException( llvh::errs(), runtime->makeHandle(runtime->getThrownValue())); } // Perform a microtask checkpoint after running script. microtask::performCheckpoint(*runtime); if (!ctx.tasksEmpty()) { vm::GCScopeMarkerRAII marker{scope}; // Run the tasks until there are no more. vm::MutableHandle<vm::Callable> task{*runtime}; while (auto optTask = ctx.dequeueTask()) { task = std::move(*optTask); auto callRes = vm::Callable::executeCall0( task, *runtime, vm::Runtime::getUndefinedValue(), false); if (LLVM_UNLIKELY(callRes == vm::ExecutionStatus::EXCEPTION)) { threwException = true; llvh::outs().flush(); runtime->printException( llvh::errs(), runtime->makeHandle(runtime->getThrownValue())); break; } // Perform a microtask checkpoint at the end of every task tick. microtask::performCheckpoint(*runtime); } } if (options.timeLimit > 0) { vm::TimeLimitMonitor::getInstance().unwatchRuntime(*runtime); } #ifdef HERMESVM_PROFILER_OPCODE runtime->dumpOpcodeStats(llvh::outs()); #endif #ifdef HERMESVM_PROFILER_JSFUNCTION runtime->dumpJSFunctionStats(); #endif #ifdef HERMESVM_PROFILER_EXTERN if (options.patchProfilerSymbols) { patchProfilerSymbols(runtime.get()); } else { dumpProfilerSymbolMap(runtime.get(), options.profilerSymbolsFile); } #endif #ifdef HERMESVM_PROFILER_NATIVECALL runtime->dumpNativeCallStats(llvh::outs()); #endif if (shouldRecordGCStats) { llvh::errs() << "Process stats:\n"; statSampler->stop().printJSON(llvh::errs()); if (options.forceGCBeforeStats) { runtime->collect("forced for stats"); } printStats(*runtime, llvh::errs()); } #ifdef HERMESVM_PROFILER_BB if (options.basicBlockProfiling) { runtime->getBasicBlockExecutionInfo().dump(llvh::errs()); } #endif return !threwException; } } // namespace /// Executes the HBC bytecode provided in HermesVM. /// \return true on success, false on error. bool executeHBCBytecode( std::shared_ptr<hbc::BCProvider> &&bytecode, const ExecuteOptions &options, const std::string *filename) { return maybeCatchException([&] { return executeHBCBytecodeImpl(std::move(bytecode), options, filename); }); } } // namespace hermes