blob: 0f1760ecf767e73648d826d6f917e83b50bce436 [file] [log] [blame]
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/script/mozjs/mozjs_engine.h"
#include <algorithm>
#include <string>
#include "base/debug/trace_event.h"
#include "base/file_path.h"
#include "base/logging.h"
#include "base/message_loop.h"
#include "cobalt/base/c_val.h"
#include "cobalt/browser/stack_size_constants.h"
#include "cobalt/script/mozjs/mozjs_global_environment.h"
#include "cobalt/script/mozjs/util/stack_trace_helpers.h"
#include "third_party/mozjs/cobalt_config/include/jscustomallocator.h"
#include "third_party/mozjs/js/src/jsapi.h"
#include "third_party/mozjs/js/src/jsdbgapi.h"
namespace cobalt {
namespace script {
namespace mozjs {
namespace {
// Trigger garbage collection this many seconds after the last one.
const int kGarbageCollectionIntervalSeconds = 60;
JSBool CheckAccessStub(JSContext*, JS::Handle<JSObject*>, JS::Handle<jsid>,
JSAccessMode, JS::MutableHandle<JS::Value>) {
return true;
}
JSSecurityCallbacks security_callbacks = {
CheckAccessStub,
MozjsGlobalEnvironment::CheckEval
};
class EngineStats {
public:
EngineStats();
static EngineStats* GetInstance() {
return Singleton<EngineStats,
StaticMemorySingletonTraits<EngineStats> >::get();
}
void EngineCreated() { ++engine_count_; }
void EngineDestroyed() { --engine_count_; }
size_t UpdateMemoryStatsAndReturnReserved() {
// Accessing CVals triggers a lock, so rely on local variables when
// possible to avoid unecessary locking.
size_t allocated_memory =
MemoryAllocatorReporter::Get()->GetCurrentBytesAllocated();
size_t mapped_memory =
MemoryAllocatorReporter::Get()->GetCurrentBytesMapped();
allocated_memory_ = allocated_memory;
mapped_memory_ = mapped_memory;
return allocated_memory + mapped_memory;
}
private:
base::CVal<int> engine_count_;
base::CVal<base::cval::SizeInBytes, base::CValPublic> allocated_memory_;
base::CVal<base::cval::SizeInBytes, base::CValPublic> mapped_memory_;
};
EngineStats::EngineStats()
: engine_count_("Count.JS.Engine", 0,
"Total JavaScript engine registered."),
allocated_memory_("Memory.JS.AllocatedMemory", 0,
"JS memory occupied by the Mozjs allocator."),
mapped_memory_("Memory.JS.MappedMemory", 0, "JS mapped memory.") {}
// Pretend we always preserve wrappers since we never call
// SetPreserveWrapperCallback anywhere else. This is necessary for
// TryPreserveReflector called by WeakMap to not crash. Disabling
// bindings to WeakMap does not appear to be an easy option because
// of its use in selfhosted.js. See bugzilla discussion linked where
// they decided to include a similar dummy in the mozjs shell.
// https://bugzilla.mozilla.org/show_bug.cgi?id=829798
bool DummyPreserveWrapperCallback(JSContext *cx, JSObject *obj) {
return true;
}
} // namespace
MozjsEngine::MozjsEngine(const Options& options)
: accumulated_extra_memory_cost_(0),
moz_options_(options) {
TRACE_EVENT0("cobalt::script", "MozjsEngine::MozjsEngine()");
// TODO: Investigate the benefit of helper threads and things like
// parallel compilation.
runtime_ = JS_NewRuntime(moz_options_.js_options.gc_threshold_bytes,
JS_NO_HELPER_THREADS);
CHECK(runtime_);
// Sets the size of the native stack that should not be exceeded.
// Setting three quarters of the web module stack size to ensure that native
// stack won't exceed the stack size.
JS_SetNativeStackQuota(runtime_,
cobalt::browser::kWebModuleStackSize / 4 * 3);
JS_SetRuntimePrivate(runtime_, this);
JS_SetSecurityCallbacks(runtime_, &security_callbacks);
// Use incremental garbage collection.
JS_SetGCParameter(runtime_, JSGC_MODE, JSGC_MODE_INCREMENTAL);
// Allow Spidermonkey to allocate as much memory as it needs. If this limit
// is set we could limit the memory used by JS and "gracefully" restart,
// for example.
JS_SetGCParameter(runtime_, JSGC_MAX_BYTES, 0xffffffff);
// Callback to be called whenever a JSContext is created or destroyed for this
// JSRuntime.
JS_SetContextCallback(runtime_, &MozjsEngine::ContextCallback);
// Callback to be called at different points during garbage collection.
JS_SetGCCallback(runtime_, &MozjsEngine::GCCallback);
// Callback to be called during garbage collection during the sweep phase.
JS_SetFinalizeCallback(runtime_, &MozjsEngine::FinalizeCallback);
js::SetPreserveWrapperCallback(runtime_, DummyPreserveWrapperCallback);
EngineStats::GetInstance()->EngineCreated();
if (MessageLoop::current()) {
gc_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(
kGarbageCollectionIntervalSeconds),
this, &MozjsEngine::TimerGarbageCollect);
}
}
MozjsEngine::~MozjsEngine() {
DCHECK(thread_checker_.CalledOnValidThread());
EngineStats::GetInstance()->EngineDestroyed();
JS_DestroyRuntime(runtime_);
}
scoped_refptr<GlobalEnvironment> MozjsEngine::CreateGlobalEnvironment() {
TRACE_EVENT0("cobalt::script", "MozjsEngine::CreateGlobalEnvironment()");
DCHECK(thread_checker_.CalledOnValidThread());
return new MozjsGlobalEnvironment(runtime_, moz_options_.js_options);
}
void MozjsEngine::CollectGarbage() {
TRACE_EVENT0("cobalt::script", "MozjsEngine::CollectGarbage()");
DCHECK(thread_checker_.CalledOnValidThread());
JS_GC(runtime_);
}
void MozjsEngine::ReportExtraMemoryCost(size_t bytes) {
DCHECK(thread_checker_.CalledOnValidThread());
accumulated_extra_memory_cost_ += bytes;
const bool do_collect_garbage = accumulated_extra_memory_cost_ >
moz_options_.js_options.gc_threshold_bytes;
if (do_collect_garbage) {
accumulated_extra_memory_cost_ = 0;
CollectGarbage();
}
}
bool MozjsEngine::RegisterErrorHandler(JavaScriptEngine::ErrorHandler handler) {
error_handler_ = handler;
JSDebugErrorHook hook = ErrorHookCallback;
void* closure = this;
JS_SetDebugErrorHook(runtime_, hook, closure);
return true;
}
void MozjsEngine::TimerGarbageCollect() {
TRACE_EVENT0("cobalt::script", "MozjsEngine::TimerGarbageCollect()");
CollectGarbage();
}
JSBool MozjsEngine::ContextCallback(JSContext* context, unsigned context_op) {
JSRuntime* runtime = JS_GetRuntime(context);
MozjsEngine* engine =
static_cast<MozjsEngine*>(JS_GetRuntimePrivate(runtime));
DCHECK(engine->thread_checker_.CalledOnValidThread());
if (context_op == JSCONTEXT_NEW) {
engine->contexts_.push_back(context);
} else if (context_op == JSCONTEXT_DESTROY) {
ContextVector::iterator it =
std::find(engine->contexts_.begin(), engine->contexts_.end(), context);
if (it != engine->contexts_.end()) {
engine->contexts_.erase(it);
}
}
return true;
}
void MozjsEngine::GCCallback(JSRuntime* runtime, JSGCStatus status) {
MozjsEngine* engine =
static_cast<MozjsEngine*>(JS_GetRuntimePrivate(runtime));
if (status == JSGC_END) {
engine->accumulated_extra_memory_cost_ = 0;
// Reset the GC timer to avoid having the timed GC come soon after this one.
if (engine->gc_timer_.IsRunning()) {
engine->gc_timer_.Reset();
}
}
for (int i = 0; i < engine->contexts_.size(); ++i) {
MozjsGlobalEnvironment* global_environment =
MozjsGlobalEnvironment::GetFromContext(engine->contexts_[i]);
if (status == JSGC_BEGIN) {
TRACE_EVENT_BEGIN0("cobalt::script", "SpiderMonkey Garbage Collection");
global_environment->BeginGarbageCollection();
} else if (status == JSGC_END) {
global_environment->EndGarbageCollection();
TRACE_EVENT_END0("cobalt::script", "SpiderMonkey Garbage Collection");
}
}
}
void MozjsEngine::FinalizeCallback(JSFreeOp* free_op, JSFinalizeStatus status,
JSBool is_compartment) {
TRACE_EVENT0("cobalt::script", "MozjsEngine::FinalizeCallback()");
MozjsEngine* engine =
static_cast<MozjsEngine*>(JS_GetRuntimePrivate(free_op->runtime()));
DCHECK(engine->thread_checker_.CalledOnValidThread());
if (status == JSFINALIZE_GROUP_START) {
for (int i = 0; i < engine->contexts_.size(); ++i) {
MozjsGlobalEnvironment* global_environment =
MozjsGlobalEnvironment::GetFromContext(engine->contexts_[i]);
global_environment->DoSweep();
}
}
}
JSBool MozjsEngine::ErrorHookCallback(JSContext* context, const char* message,
JSErrorReport* report, void* closure) {
MozjsEngine* this_ptr = static_cast<MozjsEngine*>(closure);
return this_ptr->ReportJSError(context, message, report);
}
JSBool MozjsEngine::ReportJSError(JSContext* context, const char* message,
JSErrorReport* report) {
const bool is_invalid =
error_handler_.is_null() || !report || !report->filename;
if (is_invalid) {
return true; // Allow error to propagate in the mozilla engine.
}
// Report errors, warnings and uncaught exceptions. All other errors
// (like strict warnings) are ignored.
const bool do_report_error =
(report->flags == JSREPORT_ERROR) ||
(report->flags & JSREPORT_WARNING) ||
(report->errorNumber == JSMSG_UNCAUGHT_EXCEPTION);
if (do_report_error) {
std::string file_name = report->filename;
// Line/column can be zero for internal javascript exceptions. In this
// case set the values to 1, otherwise the base::SourceLocation object
// below will dcheck.
int line = std::max<int>(1, report->lineno);
int column = std::max<int>(1, report->column);
if (file_name.empty()) {
file_name = "<internal exception>";
}
base::SourceLocation source_location(file_name, line, column);
error_handler_.Run(source_location, message);
}
return true; // Allow error to propagate in the mozilla engine.
}
} // namespace mozjs
scoped_ptr<JavaScriptEngine> JavaScriptEngine::CreateEngine(
const JavaScriptEngine::Options& options) {
TRACE_EVENT0("cobalt::script", "JavaScriptEngine::CreateEngine()");
mozjs::MozjsEngine::Options moz_options(options);
return make_scoped_ptr<JavaScriptEngine>(
new mozjs::MozjsEngine(moz_options));
}
size_t JavaScriptEngine::UpdateMemoryStatsAndReturnReserved() {
return mozjs::EngineStats::GetInstance()
->UpdateMemoryStatsAndReturnReserved();
}
} // namespace script
} // namespace cobalt