diff -r 000000000000 -r 6474c204b198 dom/base/Console.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/base/Console.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1646 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozilla/dom/Console.h" +#include "mozilla/dom/ConsoleBinding.h" + +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/Maybe.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDocument.h" +#include "nsDOMNavigationTiming.h" +#include "nsGlobalWindow.h" +#include "nsJSUtils.h" +#include "nsPerformance.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "xpcprivate.h" +#include "nsContentUtils.h" + +#include "nsIConsoleAPIStorage.h" +#include "nsIDOMWindowUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsILoadContext.h" +#include "nsIServiceManager.h" +#include "nsISupportsPrimitives.h" +#include "nsIWebNavigation.h" + +// The maximum allowed number of concurrent timers per page. +#define MAX_PAGE_TIMERS 10000 + +// The maximum allowed number of concurrent counters per page. +#define MAX_PAGE_COUNTERS 10000 + +// The maximum stacktrace depth when populating the stacktrace array used for +// console.trace(). +#define DEFAULT_MAX_STACKTRACE_DEPTH 200 + +// The console API methods are async and their action is executed later. This +// delay tells how much later. +#define CALL_DELAY 15 // milliseconds + +// This constant tells how many messages to process in a single timer execution. +#define MESSAGES_IN_INTERVAL 1500 + +// This tag is used in the Structured Clone Algorithm to move js values from +// worker thread to main thread +#define CONSOLE_TAG JS_SCTAG_USER_MIN + +using namespace mozilla::dom::exceptions; +using namespace mozilla::dom::workers; + +namespace mozilla { +namespace dom { + +/** + * Console API in workers uses the Structured Clone Algorithm to move any value + * from the worker thread to the main-thread. Some object cannot be moved and, + * in these cases, we convert them to strings. + * It's not the best, but at least we are able to show something. + */ + +// This method is called by the Structured Clone Algorithm when some data has +// to be read. +static JSObject* +ConsoleStructuredCloneCallbacksRead(JSContext* aCx, + JSStructuredCloneReader* /* unused */, + uint32_t aTag, uint32_t aData, + void* aClosure) +{ + AssertIsOnMainThread(); + + if (aTag != CONSOLE_TAG) { + return nullptr; + } + + nsTArray* strings = static_cast*>(aClosure); + MOZ_ASSERT(strings->Length() > aData); + + JS::Rooted value(aCx); + if (!xpc::StringToJsval(aCx, strings->ElementAt(aData), &value)) { + return nullptr; + } + + JS::Rooted obj(aCx); + if (!JS_ValueToObject(aCx, value, &obj)) { + return nullptr; + } + + return obj; +} + +// This method is called by the Structured Clone Algorithm when some data has +// to be written. +static bool +ConsoleStructuredCloneCallbacksWrite(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle aObj, + void* aClosure) +{ + JS::Rooted value(aCx, JS::ObjectOrNullValue(aObj)); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return false; + } + + nsDependentJSString string; + if (!string.init(aCx, jsString)) { + return false; + } + + nsTArray* strings = static_cast*>(aClosure); + + if (!JS_WriteUint32Pair(aWriter, CONSOLE_TAG, strings->Length())) { + return false; + } + + strings->AppendElement(string); + + return true; +} + +static void +ConsoleStructuredCloneCallbacksError(JSContext* /* aCx */, + uint32_t /* aErrorId */) +{ + NS_WARNING("Failed to clone data for the Console API in workers."); +} + +JSStructuredCloneCallbacks gConsoleCallbacks = { + ConsoleStructuredCloneCallbacksRead, + ConsoleStructuredCloneCallbacksWrite, + ConsoleStructuredCloneCallbacksError +}; + +class ConsoleCallData MOZ_FINAL : public LinkedListElement +{ +public: + ConsoleCallData() + : mMethodName(Console::MethodLog) + , mPrivate(false) + , mTimeStamp(JS_Now() / PR_USEC_PER_MSEC) + , mMonotonicTimer(0) + { + MOZ_COUNT_CTOR(ConsoleCallData); + } + + ~ConsoleCallData() + { + MOZ_COUNT_DTOR(ConsoleCallData); + } + + void + Initialize(JSContext* aCx, Console::MethodName aName, + const nsAString& aString, const Sequence& aArguments) + { + mGlobal = JS::CurrentGlobalOrNull(aCx); + mMethodName = aName; + mMethodString = aString; + + for (uint32_t i = 0; i < aArguments.Length(); ++i) { + mArguments.AppendElement(aArguments[i]); + } + } + + JS::Heap mGlobal; + + Console::MethodName mMethodName; + bool mPrivate; + int64_t mTimeStamp; + DOMHighResTimeStamp mMonotonicTimer; + + nsString mMethodString; + nsTArray> mArguments; + + // Stack management is complicated, because we want to do it as + // lazily as possible. Therefore, we have the following behavior: + // 1) mTopStackFrame is initialized whenever we have any JS on the stack + // 2) mReifiedStack is initialized if we're created in a worker. + // 3) mStack is set (possibly to null if there is no JS on the stack) if + // we're created on main thread. + Maybe mTopStackFrame; + Maybe> mReifiedStack; + nsCOMPtr mStack; +}; + +// This class is used to clear any exception at the end of this method. +class ClearException +{ +public: + ClearException(JSContext* aCx) + : mCx(aCx) + { + } + + ~ClearException() + { + JS_ClearPendingException(mCx); + } + +private: + JSContext* mCx; +}; + +class ConsoleRunnable : public nsRunnable +{ +public: + ConsoleRunnable() + : mWorkerPrivate(GetCurrentThreadWorkerPrivate()) + { + MOZ_ASSERT(mWorkerPrivate); + } + + virtual + ~ConsoleRunnable() + { + } + + bool + Dispatch() + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + JSContext* cx = mWorkerPrivate->GetJSContext(); + + if (!PreDispatch(cx)) { + return false; + } + + AutoSyncLoopHolder syncLoop(mWorkerPrivate); + mSyncLoopTarget = syncLoop.EventTarget(); + + if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) { + JS_ReportError(cx, + "Failed to dispatch to main thread for the Console API!"); + return false; + } + + return syncLoop.Run(); + } + +private: + NS_IMETHOD Run() + { + AssertIsOnMainThread(); + + RunConsole(); + + nsRefPtr response = + new MainThreadStopSyncLoopRunnable(mWorkerPrivate, + mSyncLoopTarget.forget(), + true); + if (!response->Dispatch(nullptr)) { + NS_WARNING("Failed to dispatch response!"); + } + + return NS_OK; + } + +protected: + virtual bool + PreDispatch(JSContext* aCx) = 0; + + virtual void + RunConsole() = 0; + + WorkerPrivate* mWorkerPrivate; + +private: + nsCOMPtr mSyncLoopTarget; +}; + +// This runnable appends a CallData object into the Console queue running on +// the main-thread. +class ConsoleCallDataRunnable MOZ_FINAL : public ConsoleRunnable +{ +public: + ConsoleCallDataRunnable(ConsoleCallData* aCallData) + : mCallData(aCallData) + { + } + +private: + bool + PreDispatch(JSContext* aCx) MOZ_OVERRIDE + { + ClearException ce(aCx); + JSAutoCompartment ac(aCx, mCallData->mGlobal); + + JS::Rooted arguments(aCx, + JS_NewArrayObject(aCx, mCallData->mArguments.Length())); + if (!arguments) { + return false; + } + + for (uint32_t i = 0; i < mCallData->mArguments.Length(); ++i) { + if (!JS_DefineElement(aCx, arguments, i, mCallData->mArguments[i], + nullptr, nullptr, JSPROP_ENUMERATE)) { + return false; + } + } + + JS::Rooted value(aCx, JS::ObjectValue(*arguments)); + + if (!mArguments.write(aCx, value, &gConsoleCallbacks, &mStrings)) { + return false; + } + + mCallData->mArguments.Clear(); + mCallData->mGlobal = nullptr; + return true; + } + + void + RunConsole() MOZ_OVERRIDE + { + // Walk up to our containing page + WorkerPrivate* wp = mWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + AutoPushJSContext cx(wp->ParentJSContext()); + ClearException ce(cx); + + nsPIDOMWindow* window = wp->GetWindow(); + NS_ENSURE_TRUE_VOID(window); + + nsRefPtr win = static_cast(window); + NS_ENSURE_TRUE_VOID(win); + + ErrorResult error; + nsRefPtr console = win->GetConsole(error); + if (error.Failed()) { + NS_WARNING("Failed to get console from the window."); + return; + } + + JS::Rooted argumentsValue(cx); + if (!mArguments.read(cx, &argumentsValue, &gConsoleCallbacks, &mStrings)) { + return; + } + + MOZ_ASSERT(argumentsValue.isObject()); + JS::Rooted argumentsObj(cx, &argumentsValue.toObject()); + MOZ_ASSERT(JS_IsArrayObject(cx, argumentsObj)); + + uint32_t length; + if (!JS_GetArrayLength(cx, argumentsObj, &length)) { + return; + } + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted value(cx); + + if (!JS_GetElement(cx, argumentsObj, i, &value)) { + return; + } + + mCallData->mArguments.AppendElement(value); + } + + MOZ_ASSERT(mCallData->mArguments.Length() == length); + + mCallData->mGlobal = JS::CurrentGlobalOrNull(cx); + console->AppendCallData(mCallData.forget()); + } + +private: + nsAutoPtr mCallData; + + JSAutoStructuredCloneBuffer mArguments; + nsTArray mStrings; +}; + +// This runnable calls ProfileMethod() on the console on the main-thread. +class ConsoleProfileRunnable MOZ_FINAL : public ConsoleRunnable +{ +public: + ConsoleProfileRunnable(const nsAString& aAction, + const Sequence& aArguments) + : mAction(aAction) + , mArguments(aArguments) + { + } + +private: + bool + PreDispatch(JSContext* aCx) MOZ_OVERRIDE + { + ClearException ce(aCx); + + JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); + if (!global) { + return false; + } + + JSAutoCompartment ac(aCx, global); + + JS::Rooted arguments(aCx, + JS_NewArrayObject(aCx, mArguments.Length())); + if (!arguments) { + return false; + } + + for (uint32_t i = 0; i < mArguments.Length(); ++i) { + if (!JS_DefineElement(aCx, arguments, i, mArguments[i], nullptr, nullptr, + JSPROP_ENUMERATE)) { + return false; + } + } + + JS::Rooted value(aCx, JS::ObjectValue(*arguments)); + + if (!mBuffer.write(aCx, value, &gConsoleCallbacks, &mStrings)) { + return false; + } + + return true; + } + + void + RunConsole() MOZ_OVERRIDE + { + // Walk up to our containing page + WorkerPrivate* wp = mWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + AutoPushJSContext cx(wp->ParentJSContext()); + ClearException ce(cx); + + JS::Rooted global(cx, JS::CurrentGlobalOrNull(cx)); + NS_ENSURE_TRUE_VOID(global); + JSAutoCompartment ac(cx, global); + + nsPIDOMWindow* window = wp->GetWindow(); + NS_ENSURE_TRUE_VOID(window); + + nsRefPtr win = static_cast(window); + NS_ENSURE_TRUE_VOID(win); + + ErrorResult error; + nsRefPtr console = win->GetConsole(error); + if (error.Failed()) { + NS_WARNING("Failed to get console from the window."); + return; + } + + JS::Rooted argumentsValue(cx); + if (!mBuffer.read(cx, &argumentsValue, &gConsoleCallbacks, &mStrings)) { + return; + } + + MOZ_ASSERT(argumentsValue.isObject()); + JS::Rooted argumentsObj(cx, &argumentsValue.toObject()); + MOZ_ASSERT(JS_IsArrayObject(cx, argumentsObj)); + + uint32_t length; + if (!JS_GetArrayLength(cx, argumentsObj, &length)) { + return; + } + + Sequence arguments; + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted value(cx); + + if (!JS_GetElement(cx, argumentsObj, i, &value)) { + return; + } + + arguments.AppendElement(value); + } + + console->ProfileMethod(cx, mAction, arguments); + } + +private: + nsString mAction; + Sequence mArguments; + + JSAutoStructuredCloneBuffer mBuffer; + nsTArray mStrings; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(Console) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Console) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTimer) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStorage) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + + tmp->ClearConsoleData(); + +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Console) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTimer) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStorage) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + + for (ConsoleCallData* data = tmp->mQueuedCalls.getFirst(); data != nullptr; + data = data->getNext()) { + if (data->mGlobal) { + aCallbacks.Trace(&data->mGlobal, "data->mGlobal", aClosure); + } + + for (uint32_t i = 0; i < data->mArguments.Length(); ++i) { + aCallbacks.Trace(&data->mArguments[i], "data->mArguments[i]", aClosure); + } + } + +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Console) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Console) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Console) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback) +NS_INTERFACE_MAP_END + +Console::Console(nsPIDOMWindow* aWindow) + : mWindow(aWindow) + , mOuterID(0) + , mInnerID(0) +{ + if (mWindow) { + MOZ_ASSERT(mWindow->IsInnerWindow()); + mInnerID = mWindow->WindowID(); + + nsPIDOMWindow* outerWindow = mWindow->GetOuterWindow(); + MOZ_ASSERT(outerWindow); + mOuterID = outerWindow->WindowID(); + } + + if (NS_IsMainThread()) { + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "inner-window-destroyed", false); + } + } + + SetIsDOMBinding(); + mozilla::HoldJSObjects(this); +} + +Console::~Console() +{ + mozilla::DropJSObjects(this); +} + +NS_IMETHODIMP +Console::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (strcmp(aTopic, "inner-window-destroyed")) { + return NS_OK; + } + + nsCOMPtr wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + NS_ENSURE_SUCCESS(rv, rv); + + if (innerID == mInnerID) { + nsCOMPtr obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + } + + ClearConsoleData(); + mTimerRegistry.Clear(); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + } + + return NS_OK; +} + +JSObject* +Console::WrapObject(JSContext* aCx) +{ + return ConsoleBinding::Wrap(aCx, this); +} + +#define METHOD(name, string) \ + void \ + Console::name(JSContext* aCx, const Sequence& aData) \ + { \ + Method(aCx, Method##name, NS_LITERAL_STRING(string), aData); \ + } + +METHOD(Log, "log") +METHOD(Info, "info") +METHOD(Warn, "warn") +METHOD(Error, "error") +METHOD(Exception, "exception") +METHOD(Debug, "debug") + +void +Console::Trace(JSContext* aCx) +{ + const Sequence data; + Method(aCx, MethodTrace, NS_LITERAL_STRING("trace"), data); +} + +// Displays an interactive listing of all the properties of an object. +METHOD(Dir, "dir"); + +METHOD(Group, "group") +METHOD(GroupCollapsed, "groupCollapsed") +METHOD(GroupEnd, "groupEnd") + +void +Console::Time(JSContext* aCx, const JS::Handle aTime) +{ + Sequence data; + SequenceRooter rooter(aCx, &data); + + if (!aTime.isUndefined()) { + data.AppendElement(aTime); + } + + Method(aCx, MethodTime, NS_LITERAL_STRING("time"), data); +} + +void +Console::TimeEnd(JSContext* aCx, const JS::Handle aTime) +{ + Sequence data; + SequenceRooter rooter(aCx, &data); + + if (!aTime.isUndefined()) { + data.AppendElement(aTime); + } + + Method(aCx, MethodTimeEnd, NS_LITERAL_STRING("timeEnd"), data); +} + +void +Console::Profile(JSContext* aCx, const Sequence& aData) +{ + ProfileMethod(aCx, NS_LITERAL_STRING("profile"), aData); +} + +void +Console::ProfileEnd(JSContext* aCx, const Sequence& aData) +{ + ProfileMethod(aCx, NS_LITERAL_STRING("profileEnd"), aData); +} + +void +Console::ProfileMethod(JSContext* aCx, const nsAString& aAction, + const Sequence& aData) +{ + if (!NS_IsMainThread()) { + // Here we are in a worker thread. + nsRefPtr runnable = + new ConsoleProfileRunnable(aAction, aData); + runnable->Dispatch(); + return; + } + + ClearException ce(aCx); + + RootedDictionary event(aCx); + event.mAction = aAction; + + event.mArguments.Construct(); + Sequence& sequence = event.mArguments.Value(); + + for (uint32_t i = 0; i < aData.Length(); ++i) { + sequence.AppendElement(aData[i]); + } + + JS::Rooted eventValue(aCx); + if (!event.ToObject(aCx, &eventValue)) { + return; + } + + JS::Rooted eventObj(aCx, &eventValue.toObject()); + MOZ_ASSERT(eventObj); + + if (!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventValue, + JSPROP_ENUMERATE)) { + return; + } + + nsXPConnect* xpc = nsXPConnect::XPConnect(); + nsCOMPtr wrapper; + const nsIID& iid = NS_GET_IID(nsISupports); + + if (NS_FAILED(xpc->WrapJS(aCx, eventObj, iid, getter_AddRefs(wrapper)))) { + return; + } + + nsCOMPtr obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + obs->NotifyObservers(wrapper, "console-api-profiler", nullptr); + } +} + +void +Console::Assert(JSContext* aCx, bool aCondition, + const Sequence& aData) +{ + if (!aCondition) { + Method(aCx, MethodAssert, NS_LITERAL_STRING("assert"), aData); + } +} + +METHOD(Count, "count") + +void +Console::__noSuchMethod__() +{ + // Nothing to do. +} + +static +nsresult +StackFrameToStackEntry(nsIStackFrame* aStackFrame, + ConsoleStackEntry& aStackEntry, + uint32_t aLanguage) +{ + MOZ_ASSERT(aStackFrame); + + nsresult rv = aStackFrame->GetFilename(aStackEntry.mFilename); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t lineNumber; + rv = aStackFrame->GetLineNumber(&lineNumber); + NS_ENSURE_SUCCESS(rv, rv); + + aStackEntry.mLineNumber = lineNumber; + + rv = aStackFrame->GetName(aStackEntry.mFunctionName); + NS_ENSURE_SUCCESS(rv, rv); + + aStackEntry.mLanguage = aLanguage; + return NS_OK; +} + +static +nsresult +ReifyStack(nsIStackFrame* aStack, nsTArray& aRefiedStack) +{ + nsCOMPtr stack(aStack); + + while (stack) { + uint32_t language; + nsresult rv = stack->GetLanguage(&language); + NS_ENSURE_SUCCESS(rv, rv); + + if (language == nsIProgrammingLanguage::JAVASCRIPT || + language == nsIProgrammingLanguage::JAVASCRIPT2) { + ConsoleStackEntry& data = *aRefiedStack.AppendElement(); + rv = StackFrameToStackEntry(stack, data, language); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr caller; + rv = stack->GetCaller(getter_AddRefs(caller)); + NS_ENSURE_SUCCESS(rv, rv); + + stack.swap(caller); + } + + return NS_OK; +} + +// Queue a call to a console method. See the CALL_DELAY constant. +void +Console::Method(JSContext* aCx, MethodName aMethodName, + const nsAString& aMethodString, + const Sequence& aData) +{ + // This RAII class removes the last element of the mQueuedCalls if something + // goes wrong. + class RAII { + public: + RAII(LinkedList& aList) + : mList(aList) + , mUnfinished(true) + { + } + + ~RAII() + { + if (mUnfinished) { + ConsoleCallData* data = mList.popLast(); + MOZ_ASSERT(data); + delete data; + } + } + + void + Finished() + { + mUnfinished = false; + } + + private: + LinkedList& mList; + bool mUnfinished; + }; + + ConsoleCallData* callData = new ConsoleCallData(); + mQueuedCalls.insertBack(callData); + + ClearException ce(aCx); + + callData->Initialize(aCx, aMethodName, aMethodString, aData); + RAII raii(mQueuedCalls); + + if (mWindow) { + nsCOMPtr webNav = do_GetInterface(mWindow); + if (!webNav) { + return; + } + + nsCOMPtr loadContext = do_QueryInterface(webNav); + MOZ_ASSERT(loadContext); + + loadContext->GetUsePrivateBrowsing(&callData->mPrivate); + } + + uint32_t maxDepth = ShouldIncludeStackrace(aMethodName) ? + DEFAULT_MAX_STACKTRACE_DEPTH : 1; + nsCOMPtr stack = CreateStack(aCx, maxDepth); + + if (!stack) { + return; + } + + // Walk up to the first JS stack frame and save it if we find it. + do { + uint32_t language; + nsresult rv = stack->GetLanguage(&language); + if (NS_FAILED(rv)) { + return; + } + + if (language == nsIProgrammingLanguage::JAVASCRIPT || + language == nsIProgrammingLanguage::JAVASCRIPT2) { + callData->mTopStackFrame.construct(); + nsresult rv = StackFrameToStackEntry(stack, + callData->mTopStackFrame.ref(), + language); + if (NS_FAILED(rv)) { + return; + } + + break; + } + + nsCOMPtr caller; + rv = stack->GetCaller(getter_AddRefs(caller)); + if (NS_FAILED(rv)) { + return; + } + + stack.swap(caller); + } while (stack); + + if (NS_IsMainThread()) { + callData->mStack = stack; + } else { + // nsIStackFrame is not threadsafe, so we need to snapshot it now, + // before we post our runnable to the main thread. + callData->mReifiedStack.construct(); + nsresult rv = ReifyStack(stack, callData->mReifiedStack.ref()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + // Monotonic timer for 'time' and 'timeEnd' + if ((aMethodName == MethodTime || aMethodName == MethodTimeEnd)) { + if (mWindow) { + nsGlobalWindow *win = static_cast(mWindow.get()); + MOZ_ASSERT(win); + + ErrorResult rv; + nsRefPtr performance = win->GetPerformance(rv); + if (rv.Failed()) { + return; + } + + callData->mMonotonicTimer = performance->Now(); + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + TimeDuration duration = + mozilla::TimeStamp::Now() - workerPrivate->CreationTimeStamp(); + + callData->mMonotonicTimer = duration.ToMilliseconds(); + } + } + + // The operation is completed. RAII class has to be disabled. + raii.Finished(); + + if (!NS_IsMainThread()) { + // Here we are in a worker thread. The ConsoleCallData has to been removed + // from the list and it will be deleted by the ConsoleCallDataRunnable or + // by the Main-Thread Console object. + mQueuedCalls.popLast(); + + nsRefPtr runnable = + new ConsoleCallDataRunnable(callData); + runnable->Dispatch(); + return; + } + + if (!mTimer) { + mTimer = do_CreateInstance("@mozilla.org/timer;1"); + mTimer->InitWithCallback(this, CALL_DELAY, + nsITimer::TYPE_REPEATING_SLACK); + } +} + +void +Console::AppendCallData(ConsoleCallData* aCallData) +{ + mQueuedCalls.insertBack(aCallData); + + if (!mTimer) { + mTimer = do_CreateInstance("@mozilla.org/timer;1"); + mTimer->InitWithCallback(this, CALL_DELAY, + nsITimer::TYPE_REPEATING_SLACK); + } +} + +// Timer callback used to process each of the queued calls. +NS_IMETHODIMP +Console::Notify(nsITimer *timer) +{ + MOZ_ASSERT(!mQueuedCalls.isEmpty()); + + for (uint32_t i = 0; i < MESSAGES_IN_INTERVAL; ++i) { + ConsoleCallData* data = mQueuedCalls.popFirst(); + if (!data) { + break; + } + + ProcessCallData(data); + delete data; + } + + if (mQueuedCalls.isEmpty() && mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + return NS_OK; +} + +// We store information to lazily compute the stack in the reserved slots of +// LazyStackGetter. The first slot always stores a JS object: it's either the +// JS wrapper of the nsIStackFrame or the actual reified stack representation. +// The second slot is a PrivateValue() holding an nsIStackFrame* when we haven't +// reified the stack yet, or an UndefinedValue() otherwise. +enum { + SLOT_STACKOBJ, + SLOT_RAW_STACK +}; + +bool +LazyStackGetter(JSContext* aCx, unsigned aArgc, JS::Value* aVp) +{ + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + JS::Rooted callee(aCx, &args.callee()); + + JS::Value v = js::GetFunctionNativeReserved(&args.callee(), SLOT_RAW_STACK); + if (v.isUndefined()) { + // Already reified. + args.rval().set(js::GetFunctionNativeReserved(callee, SLOT_STACKOBJ)); + return true; + } + + nsIStackFrame* stack = reinterpret_cast(v.toPrivate()); + nsTArray reifiedStack; + nsresult rv = ReifyStack(stack, reifiedStack); + if (NS_FAILED(rv)) { + Throw(aCx, rv); + return false; + } + + JS::Rooted stackVal(aCx); + if (!ToJSValue(aCx, reifiedStack, &stackVal)) { + return false; + } + + MOZ_ASSERT(stackVal.isObject()); + + js::SetFunctionNativeReserved(callee, SLOT_STACKOBJ, stackVal); + js::SetFunctionNativeReserved(callee, SLOT_RAW_STACK, JS::UndefinedValue()); + + args.rval().set(stackVal); + return true; +} + +void +Console::ProcessCallData(ConsoleCallData* aData) +{ + MOZ_ASSERT(aData); + MOZ_ASSERT(NS_IsMainThread()); + + ConsoleStackEntry frame; + if (!aData->mTopStackFrame.empty()) { + frame = aData->mTopStackFrame.ref(); + } + + AutoSafeJSContext cx; + ClearException ce(cx); + RootedDictionary event(cx); + + JSAutoCompartment ac(cx, aData->mGlobal); + + event.mID.Construct(); + event.mInnerID.Construct(); + if (mWindow) { + event.mID.Value().SetAsUnsignedLong() = mOuterID; + event.mInnerID.Value().SetAsUnsignedLong() = mInnerID; + } else { + // If we are in a JSM, the window doesn't exist. + event.mID.Value().SetAsString() = NS_LITERAL_STRING("jsm"); + event.mInnerID.Value().SetAsString() = frame.mFilename; + } + + event.mLevel = aData->mMethodString; + event.mFilename = frame.mFilename; + event.mLineNumber = frame.mLineNumber; + event.mFunctionName = frame.mFunctionName; + event.mTimeStamp = aData->mTimeStamp; + event.mPrivate = aData->mPrivate; + + switch (aData->mMethodName) { + case MethodLog: + case MethodInfo: + case MethodWarn: + case MethodError: + case MethodException: + case MethodDebug: + case MethodAssert: + event.mArguments.Construct(); + event.mStyles.Construct(); + ProcessArguments(cx, aData->mArguments, event.mArguments.Value(), + event.mStyles.Value()); + break; + + default: + event.mArguments.Construct(); + ArgumentsToValueList(aData->mArguments, event.mArguments.Value()); + } + + if (aData->mMethodName == MethodGroup || + aData->mMethodName == MethodGroupCollapsed || + aData->mMethodName == MethodGroupEnd) { + ComposeGroupName(cx, aData->mArguments, event.mGroupName); + } + + else if (aData->mMethodName == MethodTime && !aData->mArguments.IsEmpty()) { + event.mTimer = StartTimer(cx, aData->mArguments[0], aData->mMonotonicTimer); + } + + else if (aData->mMethodName == MethodTimeEnd && !aData->mArguments.IsEmpty()) { + event.mTimer = StopTimer(cx, aData->mArguments[0], aData->mMonotonicTimer); + } + + else if (aData->mMethodName == MethodCount) { + event.mCounter = IncreaseCounter(cx, frame, aData->mArguments); + } + + // We want to create a console event object and pass it to our + // nsIConsoleAPIStorage implementation. We want to define some accessor + // properties on this object, and those will need to keep an nsIStackFrame + // alive. But nsIStackFrame cannot be wrapped in an untrusted scope. And + // further, passing untrusted objects to system code is likely to run afoul of + // Object Xrays. So we want to wrap in a system-principal scope here. But + // which one? We could cheat and try to get the underlying JSObject* of + // mStorage, but that's a bit fragile. Instead, we just use the junk scope, + // with explicit permission from the XPConnect module owner. If you're + // tempted to do that anywhere else, talk to said module owner first. + JSAutoCompartment ac2(cx, xpc::GetJunkScope()); + + JS::Rooted eventValue(cx); + if (!event.ToObject(cx, &eventValue)) { + return; + } + + JS::Rooted eventObj(cx, &eventValue.toObject()); + MOZ_ASSERT(eventObj); + + if (!JS_DefineProperty(cx, eventObj, "wrappedJSObject", eventValue, JSPROP_ENUMERATE)) { + return; + } + + if (ShouldIncludeStackrace(aData->mMethodName)) { + // Now define the "stacktrace" property on eventObj. There are two cases + // here. Either we came from a worker and have a reified stack, or we want + // to define a getter that will lazily reify the stack. + if (!aData->mReifiedStack.empty()) { + JS::Rooted stacktrace(cx); + if (!ToJSValue(cx, aData->mReifiedStack.ref(), &stacktrace) || + !JS_DefineProperty(cx, eventObj, "stacktrace", stacktrace, + JSPROP_ENUMERATE)) { + return; + } + } else { + JSFunction* fun = js::NewFunctionWithReserved(cx, LazyStackGetter, 0, 0, + eventObj, "stacktrace"); + if (!fun) { + return; + } + + JS::Rooted funObj(cx, JS_GetFunctionObject(fun)); + + // We want to store our stack in the function and have it stay alive. But + // we also need sane access to the C++ nsIStackFrame. So store both a JS + // wrapper and the raw pointer: the former will keep the latter alive. + JS::Rooted stackVal(cx); + nsresult rv = nsContentUtils::WrapNative(cx, aData->mStack, + &stackVal); + if (NS_FAILED(rv)) { + return; + } + + js::SetFunctionNativeReserved(funObj, SLOT_STACKOBJ, stackVal); + js::SetFunctionNativeReserved(funObj, SLOT_RAW_STACK, + JS::PrivateValue(aData->mStack.get())); + + if (!JS_DefineProperty(cx, eventObj, "stacktrace", + JS::UndefinedHandleValue, + JSPROP_ENUMERATE | JSPROP_SHARED | JSPROP_GETTER | + JSPROP_SETTER, + JS_DATA_TO_FUNC_PTR(JSPropertyOp, funObj.get()), + nullptr)) { + return; + } + } + } + + if (!mStorage) { + mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1"); + } + + if (!mStorage) { + NS_WARNING("Failed to get the ConsoleAPIStorage service."); + return; + } + + nsAutoString innerID; + innerID.AppendInt(mInnerID); + + if (NS_FAILED(mStorage->RecordEvent(innerID, eventValue))) { + NS_WARNING("Failed to record a console event."); + } + + nsXPConnect* xpc = nsXPConnect::XPConnect(); + nsCOMPtr wrapper; + const nsIID& iid = NS_GET_IID(nsISupports); + + if (NS_FAILED(xpc->WrapJS(cx, eventObj, iid, getter_AddRefs(wrapper)))) { + return; + } + + nsCOMPtr obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + nsAutoString outerID; + outerID.AppendInt(mOuterID); + + obs->NotifyObservers(wrapper, "console-api-log-event", outerID.get()); + } +} + +void +Console::ProcessArguments(JSContext* aCx, + const nsTArray>& aData, + Sequence& aSequence, + Sequence& aStyles) +{ + if (aData.IsEmpty()) { + return; + } + + if (aData.Length() == 1 || !aData[0].isString()) { + ArgumentsToValueList(aData, aSequence); + return; + } + + JS::Rooted format(aCx, aData[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, format)); + if (!jsString) { + return; + } + + nsDependentJSString string; + if (!string.init(aCx, jsString)) { + return; + } + + nsString::const_iterator start, end; + string.BeginReading(start); + string.EndReading(end); + + nsString output; + uint32_t index = 1; + + while (start != end) { + if (*start != '%') { + output.Append(*start); + ++start; + continue; + } + + ++start; + if (start == end) { + output.Append('%'); + break; + } + + if (*start == '%') { + output.Append(*start); + ++start; + continue; + } + + nsAutoString tmp; + tmp.Append('%'); + + int32_t integer = -1; + int32_t mantissa = -1; + + // Let's parse %. for %d and %f + if (*start >= '0' && *start <= '9') { + integer = 0; + + do { + integer = integer * 10 + *start - '0'; + tmp.Append(*start); + ++start; + } while (*start >= '0' && *start <= '9' && start != end); + } + + if (start == end) { + output.Append(tmp); + break; + } + + if (*start == '.') { + tmp.Append(*start); + ++start; + + if (start == end) { + output.Append(tmp); + break; + } + + // '.' must be followed by a number. + if (*start < '0' || *start > '9') { + output.Append(tmp); + continue; + } + + mantissa = 0; + + do { + mantissa = mantissa * 10 + *start - '0'; + tmp.Append(*start); + ++start; + } while (*start >= '0' && *start <= '9' && start != end); + + if (start == end) { + output.Append(tmp); + break; + } + } + + char ch = *start; + tmp.Append(ch); + ++start; + + switch (ch) { + case 'o': + case 'O': + { + if (!output.IsEmpty()) { + JS::Rooted str(aCx, JS_NewUCStringCopyN(aCx, + output.get(), + output.Length())); + if (!str) { + return; + } + + aSequence.AppendElement(JS::StringValue(str)); + output.Truncate(); + } + + JS::Rooted v(aCx); + if (index < aData.Length()) { + v = aData[index++]; + } + + aSequence.AppendElement(v); + break; + } + + case 'c': + { + if (!output.IsEmpty()) { + JS::Rooted str(aCx, JS_NewUCStringCopyN(aCx, + output.get(), + output.Length())); + if (!str) { + return; + } + + aSequence.AppendElement(JS::StringValue(str)); + output.Truncate(); + } + + if (index < aData.Length()) { + JS::Rooted v(aCx, aData[index++]); + JS::Rooted jsString(aCx, JS::ToString(aCx, v)); + if (!jsString) { + return; + } + + int32_t diff = aSequence.Length() - aStyles.Length(); + if (diff > 0) { + for (int32_t i = 0; i < diff; i++) { + aStyles.AppendElement(JS::NullValue()); + } + } + aStyles.AppendElement(JS::StringValue(jsString)); + } + break; + } + + case 's': + if (index < aData.Length()) { + JS::Rooted value(aCx, aData[index++]); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return; + } + + nsDependentJSString v; + if (!v.init(aCx, jsString)) { + return; + } + + output.Append(v); + } + break; + + case 'd': + case 'i': + if (index < aData.Length()) { + JS::Rooted value(aCx, aData[index++]); + + int32_t v; + if (!JS::ToInt32(aCx, value, &v)) { + return; + } + + nsCString format; + MakeFormatString(format, integer, mantissa, 'd'); + output.AppendPrintf(format.get(), v); + } + break; + + case 'f': + if (index < aData.Length()) { + JS::Rooted value(aCx, aData[index++]); + + double v; + if (!JS::ToNumber(aCx, value, &v)) { + return; + } + + nsCString format; + MakeFormatString(format, integer, mantissa, 'f'); + output.AppendPrintf(format.get(), v); + } + break; + + default: + output.Append(tmp); + break; + } + } + + if (!output.IsEmpty()) { + JS::Rooted str(aCx, JS_NewUCStringCopyN(aCx, output.get(), + output.Length())); + if (!str) { + return; + } + + aSequence.AppendElement(JS::StringValue(str)); + } + + // The rest of the array, if unused by the format string. + for (; index < aData.Length(); ++index) { + aSequence.AppendElement(aData[index]); + } +} + +void +Console::MakeFormatString(nsCString& aFormat, int32_t aInteger, + int32_t aMantissa, char aCh) +{ + aFormat.Append("%"); + if (aInteger >= 0) { + aFormat.AppendInt(aInteger); + } + + if (aMantissa >= 0) { + aFormat.Append("."); + aFormat.AppendInt(aMantissa); + } + + aFormat.Append(aCh); +} + +void +Console::ComposeGroupName(JSContext* aCx, + const nsTArray>& aData, + nsAString& aName) +{ + for (uint32_t i = 0; i < aData.Length(); ++i) { + if (i != 0) { + aName.AppendASCII(" "); + } + + JS::Rooted value(aCx, aData[i]); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return; + } + + nsDependentJSString string; + if (!string.init(aCx, jsString)) { + return; + } + + aName.Append(string); + } +} + +JS::Value +Console::StartTimer(JSContext* aCx, const JS::Value& aName, + DOMHighResTimeStamp aTimestamp) +{ + if (mTimerRegistry.Count() >= MAX_PAGE_TIMERS) { + RootedDictionary error(aCx); + + JS::Rooted value(aCx); + if (!error.ToObject(aCx, &value)) { + return JS::UndefinedValue(); + } + + return value; + } + + RootedDictionary timer(aCx); + + JS::Rooted name(aCx, aName); + JS::Rooted jsString(aCx, JS::ToString(aCx, name)); + if (!jsString) { + return JS::UndefinedValue(); + } + + nsDependentJSString key; + if (!key.init(aCx, jsString)) { + return JS::UndefinedValue(); + } + + timer.mName = key; + + DOMHighResTimeStamp entry; + if (!mTimerRegistry.Get(key, &entry)) { + mTimerRegistry.Put(key, aTimestamp); + } else { + aTimestamp = entry; + } + + timer.mStarted = aTimestamp; + + JS::Rooted value(aCx); + if (!timer.ToObject(aCx, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +JS::Value +Console::StopTimer(JSContext* aCx, const JS::Value& aName, + DOMHighResTimeStamp aTimestamp) +{ + JS::Rooted name(aCx, aName); + JS::Rooted jsString(aCx, JS::ToString(aCx, name)); + if (!jsString) { + return JS::UndefinedValue(); + } + + nsDependentJSString key; + if (!key.init(aCx, jsString)) { + return JS::UndefinedValue(); + } + + DOMHighResTimeStamp entry; + if (!mTimerRegistry.Get(key, &entry)) { + return JS::UndefinedValue(); + } + + mTimerRegistry.Remove(key); + + RootedDictionary timer(aCx); + timer.mName = key; + timer.mDuration = aTimestamp - entry; + + JS::Rooted value(aCx); + if (!timer.ToObject(aCx, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +void +Console::ArgumentsToValueList(const nsTArray>& aData, + Sequence& aSequence) +{ + for (uint32_t i = 0; i < aData.Length(); ++i) { + aSequence.AppendElement(aData[i]); + } +} + +JS::Value +Console::IncreaseCounter(JSContext* aCx, const ConsoleStackEntry& aFrame, + const nsTArray>& aArguments) +{ + ClearException ce(aCx); + + nsAutoString key; + nsAutoString label; + + if (!aArguments.IsEmpty()) { + JS::Rooted labelValue(aCx, aArguments[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, labelValue)); + + nsDependentJSString string; + if (jsString && string.init(aCx, jsString)) { + label = string; + key = string; + } + } + + if (key.IsEmpty()) { + key.Append(aFrame.mFilename); + key.Append(NS_LITERAL_STRING(":")); + key.AppendInt(aFrame.mLineNumber); + } + + uint32_t count = 0; + if (!mCounterRegistry.Get(key, &count)) { + if (mCounterRegistry.Count() >= MAX_PAGE_COUNTERS) { + RootedDictionary error(aCx); + + JS::Rooted value(aCx); + if (!error.ToObject(aCx, &value)) { + return JS::UndefinedValue(); + } + + return value; + } + } + + ++count; + mCounterRegistry.Put(key, count); + + RootedDictionary data(aCx); + data.mLabel = label; + data.mCount = count; + + JS::Rooted value(aCx); + if (!data.ToObject(aCx, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +void +Console::ClearConsoleData() +{ + while (ConsoleCallData* data = mQueuedCalls.popFirst()) { + delete data; + } +} + +bool +Console::ShouldIncludeStackrace(MethodName aMethodName) +{ + switch (aMethodName) { + case MethodError: + case MethodException: + case MethodAssert: + case MethodTrace: + return true; + default: + return false; + } +} + +} // namespace dom +} // namespace mozilla