Wow. Such Checkerboard

Since Async Pan and Zoom (APZ) has landed on Firefox OS, scrolling is now decoupled from the painting of the app we're scrolling. Because of the decoupling, when the CPU is under heavy load, we sometimes checkerboard. What do the past two sentences mean and how do we fix it? Stay with me here as we go down the rabbit hole.

What is Checkerboarding?

Checkerboarding occurs when Gecko is unable to paint the viewable portion of a webpage while we're scrolling. Instead, we just paint a color instead of the content. Visually this means we get something like this:

The blank white space at the bottom underneath 'David Ruffin' is checkerboarding.

The blank white space at the bottom underneath 'David Ruffin' is checkerboarding.

Why does this happen?

Async Pan and Zoom

Asynchronous Pan and Zoom (APZ) is a major new feature in Firefox OS that improves scrolling, panning, and zooming. Before APZ, scrolling was a synchronous affair. Every time a user touched the screen to scroll, an event would be sent to the browser essentially saying 'scroll by X amount'. Nothing else in the system could occur while the CPU painted in the newly scrolled in region. If the CPU was busy doing something else, you couldn't scroll until the CPU caught up or it would be janky.

APZ changes that by using a separate Compositor thread. Every time a user touches the screen, an event is fired off to the Compositor thread that says 'scroll by X amount'. The compositor thread is then free to scroll by X amount without being blocked by other work. For this to work smoothly, the graphics system has to overpaint what the user is currently seeing. The idea is that the user can scroll around a displayport that is larger than what the user is currently seeing (the viewport). When the user scrolls near the edge of the displayport, we can repaint the displayport again and the user scrolls as they wish. The painting of the displayport occurs on the main thread, but the scrolling occurs on the Compositor thread. Thus, we should be able to have smoother scrolling. It's all kind of difficult to explain in text, so let's checkout this video:

What we see is how the graphics subsystem works when we're scrolling. The initial 'red' box we see is the current viewport, or what the user is seeing. The darker brown box is the whole webpage. As we scroll, we see a yellow box, which is the 'requested' displayport. This is a request to the graphics system to render and paint that portion of the webpage. Finally, the green box displays what we've actually painted. As we scroll, we see that we're constantly requesting new display ports (yellow boxes), and we're flashing green as we paint in new displayports. These displayports should always be following the viewport and in theory, the viewport always fits into the green box. When they don't, we get sadface or checkerboarding. This occurs around frame ~440 (top left number) where the top of the red box is just slightly outside the green box. Not quite the best visual appearance ever is it? Finally, we know what checkboarding is and why it happens. How do we fix it?


The Graphics Pipeline

Figuring out how to fix something requires an understanding of what's actually happening. Your HTML page goes through these following steps:

  1.  Parsed HTML into a Content Tree - Mostly a 1:1 representation of your document
  2. Content Tree is translated into rectangles creating the Frame Tree
  3. Each node in the Frame Tree is assigned a Layer, creating a Layer Tree
  4. The Layer Tree is painted. The content is sent to the Compositor in another thread
  5. The Compositor renders each layer to create a single image you see on the screen.

Steps (1) and (2) occur the first time the page is loaded. Steps 3-5 occur at every single frame. If you want smooth scrolling, we paint 60 frames per second, which means we might have to do steps 3-5 every 16.6 ms consistently. When we checkerboard, it means we're not doing steps 3-5 within our 16.6 ms time frame. Since steps 1 and 2 occur only once, if we want to optimize smooth scrolling, we have to optimize steps 3-5. As with compilers, optimizing early in the pipeline usually produces a much better performance optimization than optimizing at the end of the pipeline. For example, the compositor's job is to go through each layer and draw pixels. If we can optimize a layer, the compositor doesn't have to do any work. The main way to optimize checkerboarding and smooth scrolling is to optimize the layer tree. If we optimize a layer in the layer tree, we don't have to paint anything in the main thread, we send less data over IPC to the compositor thread, and the Compositor has to do less work. Now, what is a Layer? What's a Layer Tree. And what is ???? before we Profit!

Layerize All the Things!

Every node in the Frame tree is assigned to a Layer. There are different layers for different types of content, such as an image, a video, a scrollable layer, etc. All the Layers make up the Layer Tree. The idea of a layer comes from painting by hand, where oil painters used layers for different elements in their paintings. The decision to assign content to which layer occurs in nsFrame::BuildDisplayList. At each frame, Gecko decides to invalidate certain parts of the Layer Tree and assigns those elements to a new layer. What does a Layer Tree look like?


We see that there are a ton of different types of Layers, but there are a few that stand out.

  1. The RefLayer - This is the layer that contains the content app or child process. Everything below it belongs to the content. Everything above it refers to the parent.
  2. ContainerLayers - These layers just contain other layers. Sometimes these are Scrollable layers, as identified by a ScrollID.
  3. ThebesLayers - Layers that have an allocated buffer that we can draw to. We mostly want to optimize these!
  4. ColorLayers - Blank layers made of a single color (usually specified with the CSS property background-color).

To see a text dump of a Layer Tree, enable the layers.dump preference or in the developer menu in Gaia. Our job to reduce checkerboarding is to make sure we both (1) Layerize Correctly and (2) Make as few changes to the Layer Tree as possible at each frame. How do we know when we're doing (1) or (2)?

Layerizing Correctly

There are a few things to check to see if an app is layerizing correctly. The are generally just alerts to see if we're doing ok. The first is to enable the FPS counter in the developer tools. The right most number on the display will tell us how many times we're overdrawing each pixel. In the ideal world, we would draw each pixel only once, so the number would be 100. If the number is 200, it means we're drawing each pixel twice. While you're scrolling, if the number is ~300 - ~400 for an app, we're probably doing alright. Anything over ~500 should be an alarm that we're not layering correctly.

The other option is to enable Draw Layers Borders. If we have a lot of random layers all over the place while we're scrolling, or if the layer's don't make any sense, we're buildings bad layers. Here's an old example from the Settings app where we were layerizing pretty badly. See the random green boxes around sound? Bad.

Over Invalidation

The next thing to check is to see if the app is over invalidating the layer tree. In those cases, we'll be painting something every frame when we don't have to. For example, if a piece of content is a static block with some text that never changed, it's useless work to keep painting the same text. You can check to see if your app is over painting by enabling the 'Flash Repainted Area' option. Generally, unless it's changing, it shouldn't be flashing. In addition, with APZ, when we're scrolling, only the newly scrolled in content should maybe be flashing. If everything is flashing all the time, check your app. Some good examples on over invalidation are over here.

You should check that we're both layerizing correctly and not over invalidating the app. If both look good, you might have to read a layer tree.

Reading a Layer Tree

Reading the Layer Tree is a very dense and time consuming process, especially at the beginning. Skip this if you're busy and just want to fix your app quickly. Otherwise, grab some coffee and join me for a rabbit hole. Here is an example Layer Tree from the Settings App that helped with bug 976299.

Essentially what we're looking for is a Layer that is either (a) unused (b) not visible or (c) not the right dimensions. The key things to look for are the visible sections. These dimensions are in pixel units. On this device, we're looking at a width=320px, height=480px screen. Thus anything that isn't actually visible to your eyes shouldn't be in a layer if we're optimizing it correctly. Let's take this layer tree line by line.

  1. Layer Manager - Every Layer tree is managed by a Layer manager. Can't do anything here.
  2. Because we haven't seen a RefLayer yet (line 14), we're now in the parent process. We have a ContainerLayer that is dimensions width=320, height=480, starting at coordinate x=0, y=0 (the top left corner). X is the horizontal axis. Y is the vertical axis. We see this in the [visible=< (x=0, y=0, w=320, h=480); >] portion.
  3. We have a ThebesLayer, with x=0, y=0, w=0, h=0, and it is not visible. Ideally, we wouldn't have a Thebes Layer here, but since it has no dimensions and is not visible, we're good to go.
  4. Empty Space
  5. We have a ColorLayer. ColorLayers are a single background color layer. Here in this case it is a black background rgba=(0,0,0,1), the 1 here means opacity so it's opaque. It's also not visible, so we're good to go. Opaque is good because it means anything behind this layer isn't going to be visible, so we don't have to paint the items behind it.
  6. Another ContainerLayer that has the same dimensions. Since the layer in line (2) is just a non-visible rectangle, it's ok to have this one.
  7.  We have a ThebesLayer that is width=320, height=20px located at (0, 0). Since this is in the parent process, this is the status bar that tells you how much battery you have, etc.
  8. Every Thebes layer has some buffer attached to it to actually draw the data. The ContentHost tells us it has a buffer of size width=320, height=32, located at (0, 0). In the ideal world, the buffer size would be height=20, but 32 works because of the GPU.
  9. The ContentHost has a gralloced buffer that we're painting with OpenGL. Usually, for every ThebesLayer, you should see both a ContentHost (8) and a Gralloced Buffer (9).
  10. Another Container Layer! This time located at (0, 20) (so vertically down 20 pixels, visually 20 pixels from the very top or just below the status bar), a size width=320, height=460. The height here is 460 because 480px total screen size - 20 pixels for the status bar.
  11. Another Thebes layer, not visible so we're ok.
  12. Empty
  13. A Color Layer that is the whole container layer (10). Color Layers are cheap, so it's ok. In the ideal world, we'd get rid of this too. But the important part of the Container Layer is line 14!
  14. RefLayer - This marks the beginning of the Content App, or the Settings App here. We see it starts at coordinate (0, 20), is 320x460.
  15. The Container Layer here starts at coordinate (0, 0), width=640, height=460. A few things to note here, since the ContainerLayer is inside the RefLayer, the (0, 0) is from the top left of the ContainerLayer in (10). That means, from the point of view of the whole screen, it's actually at (0, 20), width=640, height=460. In addition, it has a DisplayPort of size 640x460, which is the displayport associated with Async Pan and Zoom. Since our screen size is only width=320, we're allocating twice as much size as we need to. ALERT SHOULD BE GOING OFF HERE!
  16. We have a ThebesLayer that starts at x=320, y=0 (again, from the point of view of the ContainerLayer defined in (line 10)), so it's actually at coordinate (320, 20). The width=320, height=460. Essentially, we're having a layer that is horizontally shifted by 320 pixels. But our screen is only 320x480. We have a layer for something off screen! ALERT! See bug 976299.
  17. A Content Host for the Thebes Layer in 16, again at position (320, 0).
  18. The buffer for the Content Host and Thebes Layer.
  19. Another Container Layer! Yay! This time at (0, 0), width=320, height=460. This Layer is for the word 'Settings' and might be too big.
  20. Another ThebesLayer, (0, 0) 320x460. Again in reality, it's at (0, 20), but because it's inside line 10, it's from the point of view of shifted down 20 pixels.
  21. Another Content Host.
  22. Another buffer for the Thebes Layer in line 20.
  23. Another Container Layer. However, this is the layer we're actually scrolling. First, we see that it has dimensions (0, 50) width=320, height=1435. Why is it height=50? What's at (0, 0) in the Settings app? Oh, the word 'Settings'! Thus the layer at line 19 contains the word 'Settings'. This layer at line 23, is the scrollable area of the Settings App. The height=1435 is because of APZ. Remember the displayport being bigger than the viewport? Here's how big the displayport is! 1435 pixels. We see that this height=1435 matches the displayport size [displayport=(x=0.000000, y=0.000000, w=320.000000, h=1435.000000)].
  24. A Thebes Layer for the whole displayport, hence 320x1435 size. We also see again that the starting coordinate is (0 ,0), which is again relative to the ContainerLayer in line 23. So (0, 0), means the top left of the ContainerLayer in line 23, or in overall coordinate space (0, 70). 20 from the status bar plus 50 from the word 'Settings'.
  25. A ContentHost for the ThebesLayer in line 24. The same size.
  26. A buffer for the ContentHost for the ThebesLayer in 24.

Whew. Thus when we're optimizing the Layer Tree, we're looking for items that are allocated a layer when they shouldn't be or wrong sized layers. One thing to note here is that every layer here is Opaque. If it is transparent, we'll see [component-alpha] instead of [OpaqueContent]. Opaque content is great because we don't have to paint any items behind the opaque content. If you were wondering why we were adding a bunch of background-colors into Gaia, this is it. Whew, so that's line by line on how to read a layer tree. Essentially, if you see a layer but don't see it on the screen, we can optimize a bit more. Every time we eliminate a Layer, it helps the compositor at every single frame, or every 16.6ms.

Are We There Yet?

In theory, if we fix everything, and we're not doing any other work while scrolling, we should not checkerboard. We've made a lot of progress in many of the apps. You can find the mother bug in bug 942750. Until then:


Special thanks to the Graphics, Firefox OS Performance, and Gaia teams for their hard work at fixing the problem. Special shout out to BenWa, Ben Kelly, and Vivien for their help.

The will-animate CSS Property

Update: will-animate has been renamed to will-change.

Benoit Girard has been hard at work on adding a 'will-animate' CSS property to gecko in bug 940842. Will-animate hints to the browser that some section of the webpage will animate, and thus the browser can take advantage of that knowledge and optimize the web page's smoothness. What exactly does that mean and why do we need it? There's a lot of background information required to understand what's going on in the gecko browser, so the tl;dr - A replacement for 'translateZ(0px);' that will optimize the animation you need rather than optimizing a translation.

Will-animate is a high level hint that the browsers can use to trigger optimizations for some portions of a page. For example, it can be used tell the browser that some elements will animate on a scroll or animate in general. Currently, browsers have to use heuristics to determine which elements will potentially animate, which may not always be most optimal. At the moment, it's only a high level hint and isn't a guarantee that the browser will optimize the case. It's going to be up to each browser vendor to decide how to use the property. For now, there's a prototype implementation in Gecko, so let's see what gets optimized in Gecko.

A browser does a ton of work to display a website. The software pipeline from your HTML to a pixel is long and complicated. First, the HTML gets parsed into a Content Tree. This Content Tree essentially represents the same thing as the HTML on the web page. Next, the Content Tree is converted into a Frame Tree, which represents mostly the same thing as the Content Tree, but nodes represent rectangles that are paintable. Next, we enter the Painting phase, which takes as input the Frame Tree and issues drawing commands to create a painted Layer Tree. Finally the Compositor finishes by flattening the painted Layer Tree and issues drawing commands to the GPU to create a single picture on the screen.

The Painting phase is made up of two steps: 1) Build a Layer Tree and 2) Rasterize the layers. Building a layer tree involves analyzing the Frame Tree and deciding which nodes from the Frame Tree go into which layers, creating a Layer Tree. You can think of a layer as a blank piece of paper that needs to be drawn on. Different elements of the Frame Tree are put into different layers depending on some heuristics, but the overall goal is to minimize invalidating parts of a webpage and to only have to redraw as little of the webpage as possible at each frame. Rasterizing involves issuing drawing commands to draw each layer, for example draw a line. Once each layer has finished drawing, we've finished painting the current frame and we hand off the painted Layer Tree to the Compositor. Will-animate helps with both building the Layer Tree and rasterizing the layers.

First we'll start off with how it helps with creating the Layer Tree. Most simple text web pages are a single layer, so the Layer Tree is rather simple. You can see which parts of a web page are layers by enabling the preference 'layers.draw-borders' in your about:config on Firefox. For many simple text websites, all you'll see is a single green box around the whole page, with a few green boxes around parts of Firefox itself. The whole webpage can be rendered in a single layer (e.g. front page of If you go to a media heavy page such as any video you YouTube, you should see a large red box around the video section, indicating another layer. For Firefox OS, you can see the layers by enabling the preference "Draw Layers Borders".

Without will-animate on Firefox OS, for example the Settings App, the Layer Tree is created once the application is launched and the user begins scrolling. There is a layer around the scrollable area of the Settings app and the Gecko engine does some heuristics to determine that a layer is a scrollable layer, which is then optimized by the platform layer for smooth scroll animations. However, on many of these apps, in order to save memory, the layer tree is collapsed. The scrollable layer's memory is reclaimed because nothing on the screen is moving and Gecko assumes that everything will remain static, moving Frame Tree nodes into as few layers as possible. When the user scrolls again, the layer tree has to be rebuilt and once again Gecko has to detect which part of the app will scroll and optimize for that case. You can see the layer tree collapsing in this video where the borders go away after a couple of seconds, then the layer tree being rebuilt after we scroll again:

With will-animate, we optimize for two things when building the Layer Tree. First, the heuristics to determine that a layer is scrollable go away which optimizes some of that guesswork to figure out which layer has a scrollable component. Second, it prevents Gecko from collapsing the layer, which makes subsequent scrolls start just a tad faster. You can see in this second video, the layer for the scrollable area never collapses, which means from the layout and graphics point of view, the layer tree wasn't thrown away.

Will-animate also helps the Rasterizing of the Layer Tree. As a user scrolls a web page, the Layer Tree constantly has to determine which elements of the webpage are now in view as they are scrolled in and issue draw commands to the appropriate layers to draw the newly scrolled in elements. You can see what elements are being painted in by enabling the "Flash repainted area" option. As you scroll, different elements get different colors meaning they are actively being painted. Elements that were previously on the screen stay the same color as we don't have to repaint those elements. When the Layer Tree collapses, a paint command to draw the whole screen again is issued on a single static layer. You can see a video of this here:

With will-animate, because the Layer Tree isn't collapsed, we don't have to issue the final repaint at the end of the video. Also, once we start scrolling again, we only have to paint the new elements that are scrolled in, saving us a bit of work.

Overall, for the Settings app, we save ~20ms of work, which prevents us from skipping 1 frame if we want to render things at 60 FPS (1000 ms / 60 = 16.66... ms per frame). Some initial numbers show better savings like the home screen app or a whopping 72ms for the Calendar app. Benoit has proposed will-animate as a new CSS property to www-styles, so hopefully it comes to a browser near you.


Thanks much to Benoit Girard for proofreading.

Tracking Reflows for Fun and Profit

One of the biggest performance problem's we're seeing in Firefox OS is lots of reflows. A reflow is when the layout engine needs to calculate the size of the elements in your web page. If your app changes the CSS appearance of an element or modifies the DOM, a reflow calculates how big and where elements should be laid out. Previously, figuring out when / how a reflow occurred was basically guess and check. Thanks to some awesome work by Paul Rouget, Vivien Nicolas, and Etienne Segonzac in Bug 926371. It's just become a lot easier to check when and how expensive your reflow is.

To do this on Firefox Desktop, simply enable the preference 'devtools.webconsole.filter.csslog' in about:config. Then load up Tools->Developer->Web Console and you should see some reflows occurring. I think you need a debug build though? At the moment, you'll need at least Firefox 27 to try this out (Mozilla Aurora branch).

For Firefox OS, it's a little more complicated. You have to setup the app manager, connect to your device, and click debug on the app you want to check your reflows on. This is the only currently supported way to check reflows on a Firefox OS app. Using desktop-b2g, or adb logcat just quite doesn't work. But once it does, you should get something like this:


Now you can see how much time is spent in a reflow. Awesome!

Building Firefox OS On OS X and Hamachi

At my first day at Mozilla, they gave me two brand spanking new Firefox OS phones. Of course, the first thing we do is build Firefox OS and flash the devices with the latest OS from the repositories. Of course, it's much easier said than done! There were a few extra steps that I needed to do to get the build working.

Preparing the Build
There's a nice link on what you need here. If you're lucky, you can just run B2G/scripts/ and be good to go. Then ignore this post :)

Firefox OS requires a specific version of gcc - 4.6.3, with custom patches, to build Firefox OS. Unfortunately, the script only checks that you have gcc 4.6,  not necessarily 4.6.3. However, this version of gcc wasn't quite building correctly for me. As it stands, the clang that comes with OS X as well as the clang that is released on cannot build the specific version of GCC. Only GCC can build the custom GCC!

The first step then, is to download the latest stable build of gcc. I used gcc 4.7 and recompiled and installed it by following the instructions. This version of gcc built just fine with clang 3.3. Next, I tried to build the gcc version used with Firefox OS, but I was getting a lot of errors. The main one was:

{standard input}:82:no such instruction: `vmovaps %xmm2, %xmm0'

This is because the homebrew script assumes you're using a newer Macbook Pro that supports the newer AVX instruction set provided by Intel. I had to modify the homebrew script to remove a few flags. If I did a brew --env, I'd get the following output:

brew --env
CC: /Applications/
CXX: /Applications/ => /Applications/
CFLAGS: -Os -w -pipe -march=native -Qunused-arguments -mmacosx-version-min=10.8
CXXFLAGS: -Os -w -pipe -march=native -Qunused-arguments -mmacosx-version-min=10.8
LDFLAGS: -L/usr/local/lib
PKG_CONFIG_LIBDIR: /usr/local/lib/pkgconfig:/usr/local/Library/ENV/pkgconfig/10.8:/usr/lib/pkgconfig
OBJC: /Applications/
PATH: /Library/Frameworks/Python.framework/Versions/2.7/bin:/usr/local/bin:/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/go/bin:/usr/texbin:/Users/masonchang/Projects/adt-bundle-mac-x86_64-20130911/sdk/platform-tools:/Users/masonchang/Projects/moz-git-tools:/Users/masonchang/Projects/llvm/build/Release/bin:/usr/local/Library/Contributions/cmd:/Applications/
CPATH: /usr/local/include

We can see a few things here that don't quite work well. First, the "-march=native" enables AVX features that might not be available on your computer unless you have the latest Intel processors. Next, we have a minimum OSX version of 10.8, which prevents Firefox OS from building because it'll use the 10.8 SDK libraries rather than the required 10.6 SDK libraries. I couldn't find anything on the home brew web page about changing default configurations, but we can modify the home brew formula for Firefox OS' gcc version to this: 

    ENV['CFLAGS'] = '-Os -w -pipe'
    ENV['CXXFLAGS'] = '-Os -w -pipe'
    ENV['CC'] = '/usr/local/bin/gcc'
    ENV['CXX'] = '/usr/local/bin/g++'

We change the CC and CXX flags to use our explicit gcc 4.7 rather than clang. Then we remove the CFLAGS and CXXFLAGS to be bare bones and not require a minimum OS X version or set the architecture. If we brew install gcc 4.6.3 required by Firefox OS, we should be good to go! 

Running the Build 

After these GCC steps, we should be able to run and everything should work. The next problem was while actually building Firefox OS, I got an error saying my python version was incorrect. I need python 2.7.3 but I had python 2.7.2. After some discussion with Gregor Wagner, installing the binary distribution of python 2.7.5 worked where as building Python 2.7.5 doesn't. After installing python, I was able to build. Woot, day one complete!

Getting Your Geolocation in Firefox / Firefox OS

Getting your Geolocation in a browser is a fairly easy feature to understand yet has quite a large amount of complexity to implement. First, let's go back to our starting point, we can build a web application that displays our geolocation using the Geolocation Web API. What happens underneath the covers in the browser level to actually get your geolocation?

We start by parsing some of the Javascript, and thus the SpiderMonkey VM. Since our application isn't performance critical, the JavaScript is actually interpreted in Interpreter.cpp. We hit the js::Invoke function to call our native function. A native function is a function built into the VM rather than another JavaScript function in the same script. By this time, the JavaScript interpreter has looked up the function and determined that to get the geolocation, it has to call a native C function in the VM. The function that's called is Navigator::GetGeolocation, which initiates a new Geolocation Object.

During the initialization phase of the Geolocation object, it attaches itself as an "observer", which is just a listener to a GeolocationServices object. The GeolocationServices object is what actually maintains all the information about our Geolocation. Everything discussed so far applies to both Firefox OS and Firefox Desktop. However, how we actually fetch our location now differs between the two platforms.

Firefox Desktop
Firefox Desktop on Mac is a little bit easier to explain. When Firefox requests for your location, it jumps to nsGeolocationRequest::Allow, which fires after you allow Firefox to use your location. Allow() then checks for a cached Geolocation. If it exists, it fires off a new event with your current Geolocation. This is an important underpinning in the browser and JavaScript in general - Everything is an event.

A main thread constantly checks a queue to see if an event has arrived. Once it has, it processes the event. This is called the Event Loop. Our Geolocation just creates a new event and submits it to the queue, where another thread will pick it up and process our location.

Now what happens if we don't have a cached position? Actually what happens is once we create the nsGeolocationService, Firefox uses OS X's CoreLocation service. CoreLocation is an OSX provided API to get your location. You supply CoreLocation with a callback function whenever you want to get an updated location. What Firefox does is tell CoreLocation to callback nsGeolocationService::Update, which calls nsGeoLocationService::SetCachedPosition and sets our cached location. Thus, Firefox actually always has your location due to CoreLocation. When Firefox requests your location, it just reads the cached location which is constantly being updated by OS X's CoreLocation. If you deny your location to a website, Firefox just returns an error and the website doesn't get your address, but internally, (I think) the OS X and Firefox have your location ready to reply.

Firefox OS
Firefox OS has mostly the same logic as Firefox Desktop except there is one big difference. Apps in Firefox OS, like apps on Desktop, are different processes, but core functionality such as getting your Geolocation belong to the core B2G process. Different apps talk to the core B2G process to request your Geolocation. Each app has it's own event queue that's being run.

When an app requests your location, it creates a new event requesting your Geolocation. Through IPC, the event gets pushed onto the main B2G's event loop queue. When the main B2G process handles your Geolocation request, it submits the position event back through IPC to your app. The app then gets a Geolocation update, which pushes another event to update whatever part of the app needed the location.

Finally, there is some part of the OS that has to actually call the GPS driver to get the location. I haven't found that part yet, but I'm on the hunt!