/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=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/. */ /* * Runs the main native Cocoa run loop, interrupting it as needed to process * Gecko events. */ #import #include "mozilla/AvailableMemoryWatcher.h" #include "CustomCocoaEvents.h" #include "mozilla/WidgetTraceEvent.h" #include "nsAppShell.h" #include "gfxPlatform.h" #include "nsCOMPtr.h" #include "nsIFile.h" #include "nsDirectoryServiceDefs.h" #include "nsString.h" #include "nsIRollupListener.h" #include "nsIWidget.h" #include "nsMemoryPressure.h" #include "nsThreadUtils.h" #include "nsServiceManagerUtils.h" #include "nsObjCExceptions.h" #include "nsCocoaUtils.h" #include "nsChildView.h" #include "nsToolkit.h" #include "TextInputHandler.h" #include "mozilla/BackgroundHangMonitor.h" #include "ScreenHelperCocoa.h" #include "mozilla/Hal.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/ProfilerThreadSleep.h" #include "mozilla/widget/ScreenManager.h" #include "HeadlessScreenHelper.h" #include "MOZMenuOpeningCoordinator.h" #include "pratom.h" #if !defined(RELEASE_OR_BETA) || defined(DEBUG) # include "nsSandboxViolationSink.h" #endif #include #include "nsIDOMWakeLockListener.h" #include "nsIPowerManagerService.h" #include "nsIObserverService.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_widget.h" using namespace mozilla; using namespace mozilla::widget; #define WAKE_LOCK_LOG(...) MOZ_LOG(gMacWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) static mozilla::LazyLogModule gMacWakeLockLog("MacWakeLock"); // A wake lock listener that disables screen saver when requested by // Gecko. For example when we're playing video in a foreground tab we // don't want the screen saver to turn on. class MacWakeLockListener final : public nsIDOMMozWakeLockListener { public: NS_DECL_ISUPPORTS; private: ~MacWakeLockListener() {} IOPMAssertionID mAssertionNoDisplaySleepID = kIOPMNullAssertionID; IOPMAssertionID mAssertionNoIdleSleepID = kIOPMNullAssertionID; NS_IMETHOD Callback(const nsAString& aTopic, const nsAString& aState) override { if (!aTopic.EqualsASCII("screen") && !aTopic.EqualsASCII("audio-playing") && !aTopic.EqualsASCII("video-playing")) { return NS_OK; } // we should still hold the lock for background audio. if (aTopic.EqualsASCII("audio-playing") && aState.EqualsASCII("locked-background")) { WAKE_LOCK_LOG("keep audio playing even in background"); return NS_OK; } bool shouldKeepDisplayOn = aTopic.EqualsASCII("screen") || aTopic.EqualsASCII("video-playing"); CFStringRef assertionType = shouldKeepDisplayOn ? kIOPMAssertionTypeNoDisplaySleep : kIOPMAssertionTypeNoIdleSleep; IOPMAssertionID& assertionId = shouldKeepDisplayOn ? mAssertionNoDisplaySleepID : mAssertionNoIdleSleepID; WAKE_LOCK_LOG("topic=%s, state=%s, shouldKeepDisplayOn=%d", NS_ConvertUTF16toUTF8(aTopic).get(), NS_ConvertUTF16toUTF8(aState).get(), shouldKeepDisplayOn); // Note the wake lock code ensures that we're not sent duplicate // "locked-foreground" notifications when multiple wake locks are held. if (aState.EqualsASCII("locked-foreground")) { if (assertionId != kIOPMNullAssertionID) { WAKE_LOCK_LOG("already has a lock"); return NS_OK; } // Prevent screen saver. CFStringRef cf_topic = ::CFStringCreateWithCharacters( kCFAllocatorDefault, reinterpret_cast(aTopic.Data()), aTopic.Length()); IOReturn success = ::IOPMAssertionCreateWithName(assertionType, kIOPMAssertionLevelOn, cf_topic, &assertionId); CFRelease(cf_topic); if (success != kIOReturnSuccess) { WAKE_LOCK_LOG("failed to disable screensaver"); } WAKE_LOCK_LOG("create screensaver"); } else { // Re-enable screen saver. if (assertionId != kIOPMNullAssertionID) { IOReturn result = ::IOPMAssertionRelease(assertionId); if (result != kIOReturnSuccess) { WAKE_LOCK_LOG("failed to release screensaver"); } WAKE_LOCK_LOG("Release screensaver"); assertionId = kIOPMNullAssertionID; } } return NS_OK; } }; // MacWakeLockListener // defined in nsCocoaWindow.mm extern int32_t gXULModalLevel; static bool gAppShellMethodsSwizzled = false; void OnUncaughtException(NSException* aException) { nsObjCExceptionLog(aException); MOZ_CRASH("Uncaught Objective C exception from NSSetUncaughtExceptionHandler"); } @implementation GeckoNSApplication // Load is called very early during startup, when the Objective C runtime loads this class. + (void)load { NSSetUncaughtExceptionHandler(OnUncaughtException); } // This method is called from NSDefaultTopLevelErrorHandler, which is invoked when an Objective C // exception propagates up into the native event loop. It is possible that it is also called in // other cases. - (void)reportException:(NSException*)aException { if (ShouldIgnoreObjCException(aException)) { return; } nsObjCExceptionLog(aException); #ifdef NIGHTLY_BUILD MOZ_CRASH("Uncaught Objective C exception from -[GeckoNSApplication reportException:]"); #endif } - (void)sendEvent:(NSEvent*)anEvent { mozilla::BackgroundHangMonitor().NotifyActivity(); if ([anEvent type] == NSEventTypeApplicationDefined && [anEvent subtype] == kEventSubtypeTrace) { mozilla::SignalTracerThread(); return; } [super sendEvent:anEvent]; } - (NSEvent*)nextEventMatchingMask:(NSEventMask)mask untilDate:(NSDate*)expiration inMode:(NSString*)mode dequeue:(BOOL)flag { if (expiration) { mozilla::BackgroundHangMonitor().NotifyWait(); } NSEvent* nextEvent = [super nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:flag]; if (expiration) { mozilla::BackgroundHangMonitor().NotifyActivity(); } return nextEvent; } @end // AppShellDelegate // // Cocoa bridge class. An object of this class is registered to receive // notifications. // @interface AppShellDelegate : NSObject { @private nsAppShell* mAppShell; } - (id)initWithAppShell:(nsAppShell*)aAppShell; - (void)applicationWillTerminate:(NSNotification*)aNotification; @end // nsAppShell implementation NS_IMETHODIMP nsAppShell::ResumeNative(void) { nsresult retval = nsBaseAppShell::ResumeNative(); if (NS_SUCCEEDED(retval) && (mSuspendNativeCount == 0) && mSkippedNativeCallback) { mSkippedNativeCallback = false; ScheduleNativeEventCallback(); } return retval; } nsAppShell::nsAppShell() : mAutoreleasePools(nullptr), mDelegate(nullptr), mCFRunLoop(NULL), mCFRunLoopSource(NULL), mRunningEventLoop(false), mStarted(false), mTerminated(false), mSkippedNativeCallback(false), mNativeEventCallbackDepth(0), mNativeEventScheduledDepth(0) { // A Cocoa event loop is running here if (and only if) we've been embedded // by a Cocoa app. mRunningCocoaEmbedded = [NSApp isRunning] ? true : false; } nsAppShell::~nsAppShell() { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; hal::Shutdown(); if (mMemoryPressureSource) { dispatch_release(mMemoryPressureSource); mMemoryPressureSource = nullptr; } if (mCFRunLoop) { if (mCFRunLoopSource) { ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes); ::CFRelease(mCFRunLoopSource); } if (mCFRunLoopObserver) { ::CFRunLoopRemoveObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes); ::CFRelease(mCFRunLoopObserver); } ::CFRelease(mCFRunLoop); } if (mAutoreleasePools) { NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0, "nsAppShell destroyed without popping all autorelease pools"); ::CFRelease(mAutoreleasePools); } [mDelegate release]; NS_OBJC_END_TRY_IGNORE_BLOCK } NS_IMPL_ISUPPORTS(MacWakeLockListener, nsIDOMMozWakeLockListener) mozilla::StaticRefPtr sWakeLockListener; static void AddScreenWakeLockListener() { nsCOMPtr sPowerManagerService = do_GetService(POWERMANAGERSERVICE_CONTRACTID); if (sPowerManagerService) { sWakeLockListener = new MacWakeLockListener(); sPowerManagerService->AddWakeLockListener(sWakeLockListener); } else { NS_WARNING("Failed to retrieve PowerManagerService, wakelocks will be broken!"); } } static void RemoveScreenWakeLockListener() { nsCOMPtr sPowerManagerService = do_GetService(POWERMANAGERSERVICE_CONTRACTID); if (sPowerManagerService) { sPowerManagerService->RemoveWakeLockListener(sWakeLockListener); sPowerManagerService = nullptr; sWakeLockListener = nullptr; } } void RunLoopObserverCallback(CFRunLoopObserverRef aObserver, CFRunLoopActivity aActivity, void* aInfo) { static_cast(aInfo)->OnRunLoopActivityChanged(aActivity); } void nsAppShell::OnRunLoopActivityChanged(CFRunLoopActivity aActivity) { if (aActivity == kCFRunLoopBeforeWaiting) { mozilla::BackgroundHangMonitor().NotifyWait(); } // When the event loop is in its waiting state, we would like the profiler to know that the thread // is idle. The usual way to notify the profiler of idleness would be to place a profiler label // frame with the IDLE category on the stack, for the duration of the function that does the // waiting. However, since macOS uses an event loop model where "the event loop calls you", we do // not control the function that does the waiting; the waiting happens inside CFRunLoop code. // Instead, the run loop notifies us when it enters and exits the waiting state, by calling this // function. // So we do not have a function under our control that stays on the stack for the duration of the // wait. So, rather than putting an AutoProfilerLabel on the stack, we will manually push and pop // the label frame here. // The location in the stack where this label frame is inserted is somewhat arbitrary. In // practice, the label frame will be at the very tip of the stack, looking like it's "inside" the // mach_msg_trap wait function. if (aActivity == kCFRunLoopBeforeWaiting) { using ThreadRegistration = mozilla::profiler::ThreadRegistration; ThreadRegistration::WithOnThreadRef([&](ThreadRegistration::OnThreadRef aOnThreadRef) { ProfilingStack& profilingStack = aOnThreadRef.UnlockedConstReaderAndAtomicRWRef().ProfilingStackRef(); mProfilingStackWhileWaiting = &profilingStack; uint8_t variableOnStack = 0; profilingStack.pushLabelFrame("Native event loop idle", nullptr, &variableOnStack, JS::ProfilingCategoryPair::IDLE, 0); profiler_thread_sleep(); }); } else { if (mProfilingStackWhileWaiting) { mProfilingStackWhileWaiting->pop(); mProfilingStackWhileWaiting = nullptr; profiler_thread_wake(); } } } // Init // // Loads the nib (see bug 316076c21) and sets up the CFRunLoopSource used to // interrupt the main native run loop. // // public nsresult nsAppShell::Init() { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; // No event loop is running yet (unless an embedding app that uses // NSApplicationMain() is running). NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init]; char* mozAppNoDock = PR_GetEnv("MOZ_APP_NO_DOCK"); if (mozAppNoDock && strcmp(mozAppNoDock, "") != 0) { [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; } // mAutoreleasePools is used as a stack of NSAutoreleasePool objects created // by |this|. CFArray is used instead of NSArray because NSArray wants to // retain each object you add to it, and you can't retain an // NSAutoreleasePool. mAutoreleasePools = ::CFArrayCreateMutable(nullptr, 0, nullptr); NS_ENSURE_STATE(mAutoreleasePools); bool isNSApplicationProcessType = (XRE_GetProcessType() != GeckoProcessType_RDD) && (XRE_GetProcessType() != GeckoProcessType_Socket); if (isNSApplicationProcessType) { // This call initializes NSApplication unless: // 1) we're using xre -- NSApp's already been initialized by // MacApplicationDelegate.mm's EnsureUseCocoaDockAPI(). // 2) an embedding app that uses NSApplicationMain() is running -- NSApp's // already been initialized and its main run loop is already running. [[NSBundle mainBundle] loadNibNamed:@"res/MainMenu" owner:[GeckoNSApplication sharedApplication] topLevelObjects:nil]; } mDelegate = [[AppShellDelegate alloc] initWithAppShell:this]; NS_ENSURE_STATE(mDelegate); // Add a CFRunLoopSource to the main native run loop. The source is // responsible for interrupting the run loop when Gecko events are ready. mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop]; NS_ENSURE_STATE(mCFRunLoop); ::CFRetain(mCFRunLoop); CFRunLoopSourceContext context; bzero(&context, sizeof(context)); // context.version = 0; context.info = this; context.perform = ProcessGeckoEvents; mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context); NS_ENSURE_STATE(mCFRunLoopSource); ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes); // Add a CFRunLoopObserver so that the profiler can be notified when we enter and exit the waiting // state. CFRunLoopObserverContext observerContext; PodZero(&observerContext); observerContext.info = this; mCFRunLoopObserver = ::CFRunLoopObserverCreate( kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting | kCFRunLoopExit, true, 0, RunLoopObserverCallback, &observerContext); NS_ENSURE_STATE(mCFRunLoopObserver); ::CFRunLoopAddObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes); hal::Init(); if (XRE_IsParentProcess()) { ScreenManager& screenManager = ScreenManager::GetSingleton(); if (gfxPlatform::IsHeadless()) { screenManager.SetHelper(mozilla::MakeUnique()); } else { screenManager.SetHelper(mozilla::MakeUnique()); } InitMemoryPressureObserver(); } nsresult rv = nsBaseAppShell::Init(); if (isNSApplicationProcessType && !gAppShellMethodsSwizzled) { // We should only replace the original terminate: method if we're not // running in a Cocoa embedder. See bug 604901. if (!mRunningCocoaEmbedded) { nsToolkit::SwizzleMethods([NSApplication class], @selector(terminate:), @selector(nsAppShell_NSApplication_terminate:)); } gAppShellMethodsSwizzled = true; } #if !defined(RELEASE_OR_BETA) || defined(DEBUG) if (Preferences::GetBool("security.sandbox.mac.track.violations", false)) { nsSandboxViolationSink::Start(); } #endif [localPool release]; return rv; NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); } // ProcessGeckoEvents // // The "perform" target of mCFRunLoop, called when mCFRunLoopSource is // signalled from ScheduleNativeEventCallback. // // Arrange for Gecko events to be processed on demand (in response to a call // to ScheduleNativeEventCallback(), if processing of Gecko events via "native // methods" hasn't been suspended). This happens in NativeEventCallback(). // // protected static void nsAppShell::ProcessGeckoEvents(void* aInfo) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; AUTO_PROFILER_LABEL("nsAppShell::ProcessGeckoEvents", OTHER); nsAppShell* self = static_cast(aInfo); if (self->mRunningEventLoop) { self->mRunningEventLoop = false; // The run loop may be sleeping -- [NSRunLoop runMode:...] // won't return until it's given a reason to wake up. Awaken it by // posting a bogus event. There's no need to make the event // presentable. // // But _don't_ set windowNumber to '-1' -- that can lead to nasty // weirdness like bmo bug 397039 (a crash in [NSApp sendEvent:] on one of // these fake events, because the -1 has gotten changed into the number // of an actual NSWindow object, and that NSWindow object has just been // destroyed). Setting windowNumber to '0' seems to work fine -- this // seems to prevent the OS from ever trying to associate our bogus event // with a particular NSWindow object. [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:kEventSubtypeNone data1:0 data2:0] atStart:NO]; // Previously we used to send this second event regardless of // self->mRunningEventLoop. However, that was removed in bug 1690687 for // performance reasons. It is still needed for the mRunningEventLoop case // otherwise we'll get in a cycle of sending postEvent followed by the // DummyEvent inserted by nsBaseAppShell::OnProcessNextEvent. This second // event will cause the second call to AcquireFirstMatchingEventInQueue in // nsAppShell::ProcessNextNativeEvent to return true. Which makes // nsBaseAppShell::OnProcessNextEvent call nsAppShell::ProcessNextNativeEvent // again during which it will loop until it sleeps because ProcessGeckoEvents() // won't be called for the DummyEvent. // // This is not a good approach and we should fix things up so that only // one postEvent is needed. [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:kEventSubtypeNone data1:0 data2:0] atStart:NO]; } if (self->mSuspendNativeCount <= 0) { ++self->mNativeEventCallbackDepth; self->NativeEventCallback(); --self->mNativeEventCallbackDepth; } else { self->mSkippedNativeCallback = true; } if (self->mTerminated) { // Still needed to avoid crashes on quit in most Mochitests. [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:kEventSubtypeNone data1:0 data2:0] atStart:NO]; } // Normally every call to ScheduleNativeEventCallback() results in // exactly one call to ProcessGeckoEvents(). So each Release() here // normally balances exactly one AddRef() in ScheduleNativeEventCallback(). // But if Exit() is called just after ScheduleNativeEventCallback(), the // corresponding call to ProcessGeckoEvents() will never happen. We check // for this possibility in two different places -- here and in Exit() // itself. If we find here that Exit() has been called (that mTerminated // is true), it's because we've been called recursively, that Exit() was // called from self->NativeEventCallback() above, and that we're unwinding // the recursion. In this case we'll never be called again, and we balance // here any extra calls to ScheduleNativeEventCallback(). // // When ProcessGeckoEvents() is called recursively, it's because of a // call to ScheduleNativeEventCallback() from NativeEventCallback(). We // balance the "extra" AddRefs here (rather than always in Exit()) in order // to ensure that 'self' stays alive until the end of this method. We also // make sure not to finish the balancing until all the recursion has been // unwound. if (self->mTerminated) { int32_t releaseCount = 0; if (self->mNativeEventScheduledDepth > self->mNativeEventCallbackDepth) { releaseCount = PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, self->mNativeEventCallbackDepth); } while (releaseCount-- > self->mNativeEventCallbackDepth) self->Release(); } else { // As best we can tell, every call to ProcessGeckoEvents() is triggered // by a call to ScheduleNativeEventCallback(). But we've seen a few // (non-reproducible) cases of double-frees that *might* have been caused // by spontaneous calls (from the OS) to ProcessGeckoEvents(). So we // deal with that possibility here. if (PR_ATOMIC_DECREMENT(&self->mNativeEventScheduledDepth) < 0) { PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, 0); NS_WARNING("Spontaneous call to ProcessGeckoEvents()!"); } else { self->Release(); } } NS_OBJC_END_TRY_IGNORE_BLOCK; } // WillTerminate // // Called by the AppShellDelegate when an NSApplicationWillTerminate // notification is posted. After this method is called, native events should // no longer be processed. The NSApplicationWillTerminate notification is // only posted when [NSApp terminate:] is called, which doesn't happen on a // "normal" application quit. // // public void nsAppShell::WillTerminate() { if (mTerminated) return; // Make sure that the nsAppExitEvent posted by nsAppStartup::Quit() (called // from [MacApplicationDelegate applicationShouldTerminate:]) gets run. NS_ProcessPendingEvents(NS_GetCurrentThread()); mTerminated = true; } // ScheduleNativeEventCallback // // Called (possibly on a non-main thread) when Gecko has an event that // needs to be processed. The Gecko event needs to be processed on the // main thread, so the native run loop must be interrupted. // // In nsBaseAppShell.cpp, the mNativeEventPending variable is used to // ensure that ScheduleNativeEventCallback() is called no more than once // per call to NativeEventCallback(). ProcessGeckoEvents() can skip its // call to NativeEventCallback() if processing of Gecko events by native // means is suspended (using nsIAppShell::SuspendNative()), which will // suspend calls from nsBaseAppShell::OnDispatchedEvent() to // ScheduleNativeEventCallback(). But when Gecko event processing by // native means is resumed (in ResumeNative()), an extra call is made to // ScheduleNativeEventCallback() (from ResumeNative()). This triggers // another call to ProcessGeckoEvents(), which calls NativeEventCallback(), // and nsBaseAppShell::OnDispatchedEvent() resumes calling // ScheduleNativeEventCallback(). // // protected virtual void nsAppShell::ScheduleNativeEventCallback() { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; if (mTerminated) return; // Each AddRef() here is normally balanced by exactly one Release() in // ProcessGeckoEvents(). But there are exceptions, for which see // ProcessGeckoEvents() and Exit(). NS_ADDREF_THIS(); PR_ATOMIC_INCREMENT(&mNativeEventScheduledDepth); // This will invoke ProcessGeckoEvents on the main thread. ::CFRunLoopSourceSignal(mCFRunLoopSource); ::CFRunLoopWakeUp(mCFRunLoop); NS_OBJC_END_TRY_IGNORE_BLOCK; } // Undocumented Cocoa Event Manager function, present in the same form since // at least OS X 10.6. extern "C" EventAttributes GetEventAttributes(EventRef inEvent); // ProcessNextNativeEvent // // If aMayWait is false, process a single native event. If it is true, run // the native run loop until stopped by ProcessGeckoEvents. // // Returns true if more events are waiting in the native event queue. // // protected virtual bool nsAppShell::ProcessNextNativeEvent(bool aMayWait) { bool moreEvents = false; NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; bool eventProcessed = false; NSString* currentMode = nil; if (mTerminated) return false; // Do not call -[NSApplication nextEventMatchingMask:...] when we're trying to close a native // menu. Doing so could confuse the NSMenu's closing mechanism. Instead, we try to unwind the // stack as quickly as possible and return to the parent event loop. At that point, native events // will be processed. if (MOZMenuOpeningCoordinator.needToUnwindForMenuClosing) { return false; } bool wasRunningEventLoop = mRunningEventLoop; mRunningEventLoop = aMayWait; NSDate* waitUntil = nil; if (aMayWait) waitUntil = [NSDate distantFuture]; NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop]; EventQueueRef currentEventQueue = GetCurrentEventQueue(); if (aMayWait) { mozilla::BackgroundHangMonitor().NotifyWait(); } // Only call -[NSApp sendEvent:] (and indirectly send user-input events to // Gecko) if aMayWait is true. Tbis ensures most calls to -[NSApp // sendEvent:] happen under nsAppShell::Run(), at the lowest level of // recursion -- thereby making it less likely Gecko will process user-input // events in the wrong order or skip some of them. It also avoids eating // too much CPU in nsBaseAppShell::OnProcessNextEvent() (which calls // us) -- thereby avoiding the starvation of nsIRunnable events in // nsThread::ProcessNextEvent(). For more information see bug 996848. do { // No autorelease pool is provided here, because OnProcessNextEvent // and AfterProcessNextEvent are responsible for maintaining it. NS_ASSERTION(mAutoreleasePools && ::CFArrayGetCount(mAutoreleasePools), "No autorelease pool for native event"); if (aMayWait) { currentMode = [currentRunLoop currentMode]; if (!currentMode) currentMode = NSDefaultRunLoopMode; NSEvent* nextEvent = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:waitUntil inMode:currentMode dequeue:YES]; if (nextEvent) { mozilla::BackgroundHangMonitor().NotifyActivity(); [NSApp sendEvent:nextEvent]; eventProcessed = true; } } else { // In at least 10.15, AcquireFirstMatchingEventInQueue will move 1 // CGEvent from the CGEvent queue into the Carbon event queue. Unfortunately, // once an event has been moved to the Carbon event queue it's no longer a // candidate for coalescing. This means that even if we don't remove the // event from the queue, just calling AcquireFirstMatchingEventInQueue can // cause behaviour change. Prior to bug 1690687 landing, the event that we got // from AcquireFirstMatchingEventInQueue was often our own ApplicationDefined // event. However, once we stopped posting that event on every Gecko // event we're much more likely to get a CGEvent. When we have a high // amount of load on the main thread, we end up alternating between Gecko // events and native events. Without CGEvent coalescing, the native // event events can accumulate in the Carbon event queue which will // manifest as laggy scrolling. #if 1 eventProcessed = false; break; #else // AcquireFirstMatchingEventInQueue() doesn't spin the (native) event // loop, though it does queue up any newly available events from the // window server. EventRef currentEvent = AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL, kEventQueueOptionsNone); if (!currentEvent) { continue; } EventAttributes attrs = GetEventAttributes(currentEvent); UInt32 eventKind = GetEventKind(currentEvent); UInt32 eventClass = GetEventClass(currentEvent); bool osCocoaEvent = ((eventClass == 'appl') || (eventClass == kEventClassAppleEvent) || ((eventClass == 'cgs ') && (eventKind != NSEventTypeApplicationDefined))); // If attrs is kEventAttributeUserEvent or kEventAttributeMonitored // (i.e. a user input event), we shouldn't process it here while // aMayWait is false. Likewise if currentEvent will eventually be // turned into an OS-defined Cocoa event, or otherwise needs AppKit // processing. Doing otherwise risks doing too much work here, and // preventing the event from being properly processed by the AppKit // framework. if ((attrs != kEventAttributeNone) || osCocoaEvent) { // Since we can't process the next event here (while aMayWait is false), // we want moreEvents to be false on return. eventProcessed = false; // This call to ReleaseEvent() matches a call to RetainEvent() in // AcquireFirstMatchingEventInQueue() above. ReleaseEvent(currentEvent); break; } // This call to RetainEvent() matches a call to ReleaseEvent() in // RemoveEventFromQueue() below. RetainEvent(currentEvent); RemoveEventFromQueue(currentEventQueue, currentEvent); EventTargetRef eventDispatcherTarget = GetEventDispatcherTarget(); SendEventToEventTarget(currentEvent, eventDispatcherTarget); // This call to ReleaseEvent() matches a call to RetainEvent() in // AcquireFirstMatchingEventInQueue() above. ReleaseEvent(currentEvent); eventProcessed = true; #endif } } while (mRunningEventLoop); if (eventProcessed) { moreEvents = (AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL, kEventQueueOptionsNone) != NULL); } mRunningEventLoop = wasRunningEventLoop; NS_OBJC_END_TRY_IGNORE_BLOCK; if (!moreEvents) { nsChildView::UpdateCurrentInputEventCount(); } return moreEvents; } // Run // // Overrides the base class's Run() method to call [NSApp run] (which spins // the native run loop until the application quits). Since (unlike the base // class's Run() method) we don't process any Gecko events here, they need // to be processed elsewhere (in NativeEventCallback(), called from // ProcessGeckoEvents()). // // Camino called [NSApp run] on its own (via NSApplicationMain()), and so // didn't call nsAppShell::Run(). // // public NS_IMETHODIMP nsAppShell::Run(void) { NS_ASSERTION(!mStarted, "nsAppShell::Run() called multiple times"); if (mStarted || mTerminated) return NS_OK; mStarted = true; if (XRE_IsParentProcess()) { AddScreenWakeLockListener(); } // We use the native Gecko event loop in content processes. nsresult rv = NS_OK; if (XRE_UseNativeEventProcessing()) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; [NSApp run]; NS_OBJC_END_TRY_IGNORE_BLOCK; } else { rv = nsBaseAppShell::Run(); } if (XRE_IsParentProcess()) { RemoveScreenWakeLockListener(); } return rv; } NS_IMETHODIMP nsAppShell::Exit(void) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; // This method is currently called more than once -- from (according to // mento) an nsAppExitEvent dispatched by nsAppStartup::Quit() and from an // XPCOM shutdown notification that nsBaseAppShell has registered to // receive. So we need to ensure that multiple calls won't break anything. // But we should also complain about it (since it isn't quite kosher). if (mTerminated) { NS_WARNING("nsAppShell::Exit() called redundantly"); return NS_OK; } mTerminated = true; #if !defined(RELEASE_OR_BETA) || defined(DEBUG) nsSandboxViolationSink::Stop(); #endif // Quoting from Apple's doc on the [NSApplication stop:] method (from their // doc on the NSApplication class): "If this method is invoked during a // modal event loop, it will break that loop but not the main event loop." // nsAppShell::Exit() shouldn't be called from a modal event loop. So if // it is we complain about it (to users of debug builds) and call [NSApp // stop:] one extra time. (I'm not sure if modal event loops can be nested // -- Apple's docs don't say one way or the other. But the return value // of [NSApp _isRunningModal] doesn't change immediately after a call to // [NSApp stop:], so we have to assume that one extra call to [NSApp stop:] // will do the job.) BOOL cocoaModal = [NSApp _isRunningModal]; NS_ASSERTION(!cocoaModal, "Don't call nsAppShell::Exit() from a modal event loop!"); if (cocoaModal) [NSApp stop:nullptr]; [NSApp stop:nullptr]; // A call to Exit() just after a call to ScheduleNativeEventCallback() // prevents the (normally) matching call to ProcessGeckoEvents() from // happening. If we've been called from ProcessGeckoEvents() (as usually // happens), we take care of it there. But if we have an unbalanced call // to ScheduleNativeEventCallback() and ProcessGeckoEvents() isn't on the // stack, we need to take care of the problem here. if (!mNativeEventCallbackDepth && mNativeEventScheduledDepth) { int32_t releaseCount = PR_ATOMIC_SET(&mNativeEventScheduledDepth, 0); while (releaseCount-- > 0) NS_RELEASE_THIS(); } return nsBaseAppShell::Exit(); NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); } // OnProcessNextEvent // // This nsIThreadObserver method is called prior to processing an event. // Set up an autorelease pool that will service any autoreleased Cocoa // objects during this event. This includes native events processed by // ProcessNextNativeEvent. The autorelease pool will be popped by // AfterProcessNextEvent, it is important for these two methods to be // tightly coupled. // // public NS_IMETHODIMP nsAppShell::OnProcessNextEvent(nsIThreadInternal* aThread, bool aMayWait) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; NS_ASSERTION(mAutoreleasePools, "No stack on which to store autorelease pool"); NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; ::CFArrayAppendValue(mAutoreleasePools, pool); return nsBaseAppShell::OnProcessNextEvent(aThread, aMayWait); NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); } // AfterProcessNextEvent // // This nsIThreadObserver method is called after event processing is complete. // The Cocoa implementation cleans up the autorelease pool create by the // previous OnProcessNextEvent call. // // public NS_IMETHODIMP nsAppShell::AfterProcessNextEvent(nsIThreadInternal* aThread, bool aEventWasProcessed) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; CFIndex count = ::CFArrayGetCount(mAutoreleasePools); NS_ASSERTION(mAutoreleasePools && count, "Processed an event, but there's no autorelease pool?"); const NSAutoreleasePool* pool = static_cast(::CFArrayGetValueAtIndex(mAutoreleasePools, count - 1)); ::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1); [pool release]; return nsBaseAppShell::AfterProcessNextEvent(aThread, aEventWasProcessed); NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); } void nsAppShell::InitMemoryPressureObserver() { // Testing shows that sometimes the memory pressure event is not fired for // over a minute after the memory pressure change is reflected in sysctl // values. Hence this may need to be augmented with polling of the memory // pressure sysctls for lower latency reactions to OS memory pressure. This // was also observed when using DISPATCH_QUEUE_PRIORITY_HIGH. mMemoryPressureSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL, dispatch_get_main_queue()); dispatch_source_set_event_handler(mMemoryPressureSource, ^{ dispatch_source_memorypressure_flags_t pressureLevel = dispatch_source_get_data(mMemoryPressureSource); nsAppShell::OnMemoryPressureChanged(pressureLevel); }); dispatch_resume(mMemoryPressureSource); // Initialize the memory watcher. RefPtr watcher( nsAvailableMemoryWatcherBase::GetSingleton()); } void nsAppShell::OnMemoryPressureChanged(dispatch_source_memorypressure_flags_t aPressureLevel) { // The memory pressure dispatch source is created (above) with // dispatch_get_main_queue() which always fires on the main thread. MOZ_ASSERT(NS_IsMainThread()); MacMemoryPressureLevel geckoPressureLevel; switch (aPressureLevel) { case DISPATCH_MEMORYPRESSURE_NORMAL: geckoPressureLevel = MacMemoryPressureLevel::Value::eNormal; break; case DISPATCH_MEMORYPRESSURE_WARN: geckoPressureLevel = MacMemoryPressureLevel::Value::eWarning; break; case DISPATCH_MEMORYPRESSURE_CRITICAL: geckoPressureLevel = MacMemoryPressureLevel::Value::eCritical; break; default: geckoPressureLevel = MacMemoryPressureLevel::Value::eUnexpected; } RefPtr watcher( nsAvailableMemoryWatcherBase::GetSingleton()); watcher->OnMemoryPressureChanged(geckoPressureLevel); } // AppShellDelegate implementation @implementation AppShellDelegate // initWithAppShell: // // Constructs the AppShellDelegate object - (id)initWithAppShell:(nsAppShell*)aAppShell { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; if ((self = [self init])) { mAppShell = aAppShell; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification object:NSApp]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(timezoneChanged:) name:NSSystemTimeZoneDidChangeNotification object:nil]; } return self; NS_OBJC_END_TRY_BLOCK_RETURN(nil); } - (void)dealloc { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; [[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; NS_OBJC_END_TRY_IGNORE_BLOCK; } // applicationWillTerminate: // // Notify the nsAppShell that native event processing should be discontinued. - (void)applicationWillTerminate:(NSNotification*)aNotification { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; mAppShell->WillTerminate(); NS_OBJC_END_TRY_IGNORE_BLOCK; } // applicationDidBecomeActive // // Make sure TextInputHandler::sLastModifierState is updated when we become // active (since we won't have received [ChildView flagsChanged:] messages // while inactive). - (void)applicationDidBecomeActive:(NSNotification*)aNotification { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; // [NSEvent modifierFlags] is valid on every kind of event, so we don't need // to worry about getting an NSInternalInconsistencyException here. NSEvent* currentEvent = [NSApp currentEvent]; if (currentEvent) { TextInputHandler::sLastModifierState = [currentEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; } nsCOMPtr observerService = services::GetObserverService(); if (observerService) { observerService->NotifyObservers(nullptr, NS_WIDGET_MAC_APP_ACTIVATE_OBSERVER_TOPIC, nullptr); } NS_OBJC_END_TRY_IGNORE_BLOCK; } - (void)timezoneChanged:(NSNotification*)aNotification { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; nsBaseAppShell::OnSystemTimezoneChange(); NS_OBJC_END_TRY_IGNORE_BLOCK; } @end // We hook terminate: in order to make OS-initiated termination work nicely // with Gecko's shutdown sequence. (Two ways to trigger OS-initiated // termination: 1) Quit from the Dock menu; 2) Log out from (or shut down) // your computer while the browser is active.) @interface NSApplication (MethodSwizzling) - (void)nsAppShell_NSApplication_terminate:(id)sender; @end @implementation NSApplication (MethodSwizzling) // Called by the OS after [MacApplicationDelegate applicationShouldTerminate:] // has returned NSTerminateNow. This method "subclasses" and replaces the // OS's original implementation. The only thing the orginal method does which // we need is that it posts NSApplicationWillTerminateNotification. Everything // else is unneeded (because it's handled elsewhere), or actively interferes // with Gecko's shutdown sequence. For example the original terminate: method // causes the app to exit() inside [NSApp run] (called from nsAppShell::Run() // above), which means that nothing runs after the call to nsAppStartup::Run() // in XRE_Main(), which in particular means that ScopedXPCOMStartup's destructor // and NS_ShutdownXPCOM() never get called. - (void)nsAppShell_NSApplication_terminate:(id)sender { [[NSNotificationCenter defaultCenter] postNotificationName:NSApplicationWillTerminateNotification object:NSApp]; } @end