free geoip Why Are Modern Browsers Such Memory Hogs? A Short Primer on Processes, DLL's, and Threads - Jayson's Blog

Why Are Modern Browsers Such Memory Hogs? A Short Primer on Processes, DLL's, and Threads

I normally don't like to participate in the so called "browser wars" and that is not the intention of this post. But, I feel that the title poses a legitimate question: What is going on with modern browsers sucking down memory like tequila shots these days? As a web developer, I have a slew of browsers installed on my machines for testing:

  • Internet Explorer (unfortunately): Necessary evil, plus it's the only browser that supports Windows Integrated Authentication, which is mandatory on the business LAN…but in all honesty, it has gotten much better over the years.
  • Firefox: I'm a creature of habit, and have been using Firefox since the early betas, so over a decade. Plus, the plug-in support is phenomenal as are the built in web developer tools.
  • Opera: I'll be honest, I abhor Opera. Great idea (and I feel quite the opposite about their mobile browser on iPhone, it's fantastic, mainly because Opera routes web pages through their servers and compress them down to a fraction of their original size, super speedy).
  • Chrome: I'm neutral on Chrome. I love the V8 JavaScript engine…blazing fast, but I just don't really like Chrome in general. Of course the Google integration is nice, but I don't use that many consumer Google products to make the integration worthwhile.

Another thing this post isn't meant to be is an all out comparison of different browsers as that is just too objective to even begin to approach. Really I just want to pose the question as to why these browsers are such memory hogs.

A little history: When the internet came out, you had three choices of browsers: IE, Netscape, and AOL (which I believe was initially based on Netscape's Gecko engine, then later they licensed the Trident engine from Microsoft's IE). I used Netscape. Then, Netscape ceased to be, and a bunch of their developers went on to start writing Firefox as a completely free and open source solution based on the Gecko engine. 1.0 was released in late 2003, and it was an instant hit, namely due to the extensive library of plugins available. Plugins are now the industry norm, but back then it was a novel idea. Chrome wouldn't be released by Google for another 5 years (in beta) and the final version wasn't released until 2010. It quickly took off though, and depending on what browser stat site you visit, it's number one or two (I don't believe any of these sites by the way). Chrome runs on the WebKit engine, which powers Apple's Safari browser as well. One aspect Chrome really has going for it is the V8 JavaScript engine; it absolutely SMOKES the competition, mainly because it compiles JavaScript into machine code instead of bytecode.

Back to Firefox: Since day one, it has always been known as a memory hog. In 2003, 4gb RAM was pretty much the standard maximum amount, unless you were willing to take the 64bit plunge with Windows XP 64. Windows 32bit can only address ~3.5gb of memory. This is due to Physical Address Extension limitations in the Windows Kernel, but that's another post. Regardless, Firefox with a few plugins installed could easily soar to half a gig of memory under prolonged use. When Firefox introduced tabs (another browser first), memory went through the roof. But, tabs were a nice addition rather than having to open up numerous instances of a browser. The downside is that if one tab decided to misbehave, it tore down the entire browser with it since they all ran in the same memory space as a single .exe process. Multiple instances of a browser each had it's own process space, thus if one instance acted up, it only took that one instance down with it.

Then Google came out with Chrome a few years later, with a great idea (which in hindsight I'm shocked wasn't thought of sooner): Host each tab in its own process space, that way if one tab crashes, it won't take the browser down with it, and it can spin itself back up. To see this in action, if you have Chrome installed, fire up Task Manager and look for all the Chrome processes. There will be one per tab, and one per extension…each is sandboxed and can't interfere with any other process. Firefox would later take a similar approach, but only with tabs (more on this in a bit) in later releases of Firefox. Competition benefits the consumer, and to be honest, Chrome is the best thing that ever happened to Firefox, and vice-versa. Both browsers are extremely mature now, leaving IE in the dust.

Sidenote: Why haven't I mentioned IE's memory consumption? Simple…there really isn't a way that I know of to actually figure out how much memory IE uses. A common misconception about IE is that it is much leaner than Chrome or Firefox, this can be gleaned by firing up IE, and looking at the memory consumption in Task Manager; it's usually a fraction of the other two. And it spins up much faster. But, both of these are misleading: IE is completely baked into Windows, and its Trident rendering engine is used by Windows in a bunch of different areas:

  • Windows Explorer uses Trident for rendering the folder views.
  • Windows' built in help uses Trident.
  • Office uses Trident
  • This list is already long enough…

Point being, most of IE's memory consumption is part of other processes and can't be gauged very easily. Also, the reason it appears to spin up so fast is that the majority of IE's spinning up occurs during the Windows startup process: Most of IE is paged into memory by the time you get to the desktop. But I digress.

For the less technically inclined that may stumble across this post, a brief explanation of what a process is, and why they are safe, as well as some caveats. When you open an executable such as IE, Chrome, or Firefox, Windows loads the process .exe into a dedicated space in memory, which is what you see in Task Manager. Processes are completely isolated from each other, this is enforced by Windows. Think of it like an apartment complex: There is a wall between each .exe, and each .exe occupies just one address space. Processes almost always load Dynamic Link Libraries (dll's, which I'm sure everyone is familiar with) into their own address space, which is why you don't see them listed in Task Manager (though numerous tools exist that allow you to see which dll's an exe has loaded, handy for debugging). Think of a dll as the furniture in your apartment; they are part of the apartment itself. If a single .exe process fails, it cannot take other processes down with it, they are isolated. I equate this to an apartment fire (loosely): If your neighbor's apartment catches fire, it should be contained by the firewalls between the other apartments, and is contained (theoretically at least). However, if your furniture catches fire, it's going to take the apartment down with it; if a dll misbehaves, it will take the entire .exe process it's loaded into down as well. There are ways to code around this, but it's trickery and easy to do wrong.

The above is why all the major browser vendors have moved to the isolated .exe design paradigm. But, it comes at a cost. Dll's exist for a couple of reasons:

  • They are reusable chunks of code. Multiple applications can share a single dll's logic without having to have their own copy. This is why the Windows registry exists: If a DLL is registered properly, it gets a unique identifier (a GUID) called a CLSID (Class ID), which in turn can be looked up by a much friendlier text identifier called a ProgID. This is well beyond the scope of this post, just know that Windows exposes thousands of dll's that applications can use without having to reinvent the wheel.
  • Since they are loaded into the same memory address range as the .exe that needs it, they are fast. Orders of magnitude faster than cross process calls, or cross thread calls.
  • Dll's (if programmed correctly against the COM specification) are guaranteed to be usable by any other component on the machine, or even across machines. Again, beyond the scope of this article.

The biggest limitation is the one I mentioned above, and if two applications want to use the same dll, each has its own copy loaded into memory, so cross dll communication between processes is very difficult to do, and frowned upon.

Exe's are great in that they are isolated, but they come at a huge cost: Much higher overhead. I won't go into the details as to what Windows has to do to spin up a process, but it's very complicated. They use much more memory than dll's. And the biggest overhead is making cross process calls. If you look at Chrome's built in task manager (right click the title bar and choose Task Manager) you'll see something like the following (and if you really want to geek out, type chrome://memory-redirect/ in a new tab…lots of useful memory information). This is what mine looks like with one tab open (HootSuite) having only been open for 30 seconds or so. That's a lot of memory consumption. Start opening new tabs, and use it for a couple of hours, and it gets out of control fairly quickly. Yes, I have a lot of extensions installed, but I use them all. Seeing this is what prompted me to write this post actually, and I wouldn't have even noticed it had my computer not started paging to disk like crazy after opening Chrome to test a bit of code:

Each entry has a corresponding entry in Task Manager, which means each is running in its own process space. The Browser process is like the leasing office at an apartment complex: It has "keys" to each of the child processes (apartments), but they don't have keys to the office. And of course, they don't have keys to each other. If the Browser process fails, Chrome crashes (though I've never seen this happen). If any of the other processes crash, only it goes down, rebuilds itself, then spins back up and tells Browser to load it back into the browser window (ideally at least). For the most part, this is a bulletproof design. But like I mentioned above, it's the cross process communication that makes performance suffer. Back to the apartment metaphor: If you want to move the furniture around in your apartment (the dll's) you just get up and move it, almost no effort required. But, if you have a leaky faucet and need the office (Browser process) to fix it, you have to put more effort into it by either calling them, physically going to the office, etc.…and it takes a lot more time to get accomplished: They have to put in a work order, get it scheduled, get the parts they need, etc. And tab processes further complicate the issue, view tabs as a group of apartments inside the overall complex itself. Plugins go through the tab process(es) to get to the Browser (usually), so that's 3 layers: Browser holds multiple Tabs, Tabs hold multiple Extensions…memory starts adding up quickly.

This is the equivalent to a cross process call: For the child process to communicate with the coordinating process (Browser) takes a lot of time, and the coordinator is busy busy busy, so if it is busy handling another request, the callees have to wait (kind of…multi-threading solves a lot of these problems, but with a completely different set of ramifications that I won't even begin to get into…threading is an extremely difficult programming concept that I still struggle with from time to time, see the addendum at the end of this post for a further apartment analogy). These calls incur quite a bit of resource overhead: Processing cycles, memory consumption, and time.

Ok enough of the history lesson and technical mumbo jumbo. And for the record, Firefox is memory heavy as well, but about half of Chrome with the equivalent tab open, and about twice as many extensions. It grows over time, but not as nearly as much. So, you'd think that over the years, these browsers would be lean mean rendering machines. But, memory consumption has gone up, not down. I have a few hypothesis about this:

  • Extensions, plain and simple. I have never written a browser extension, but most of them make heavy use of JavaScript, which is processed by the browser itself. And many extensions are authored by what I like to call "basement coders"…most of my extensions are written by seasoned professionals, but you never know. A badly written extension can send memory consumption through the roof.
  • Web pages themselves. Now that bandwidth is plentiful, browsers are maturing, and computers are much more powerful, the browsers do the bulk of the work for websites these days. I'm sure most readers of this blog have a Facebook account. Go to Facebook, right click anywhere in the page, and choose View Source. That is a lot of code for the browser to parse through for each request, and now with Ajax technologies, the browser is constantly doing work behind the scenes via JavaScript. All of this requires memory. Of course this is desirable, we get an extremely rich browsing experience, gone are the days of static HTML and boring horribly designed websites.
  • And finally (I keep mentioning this in other posts): Because they can. Memory is cheap, bandwidth is plentiful…that's no excuse in my opinion, but it is what it is.

Don't get me wrong, I like Chrome, but I disagree with their architecture. Per tab processes is a no brainer, but a process per extension? I disagree with the design of that (but not the motivation behind it). Firefox uses the tab per process design, but lumps all of its extensions into one extra process, Yes, if one extension goes down, it'll take all the other extensions down with it, but Firefox will rebuild that process (which takes more time than one individual process) and recover nicely. Without taking down the offending tab, or the Firefox process itself. In Firefox, plugins do get their own process (not to be confused with extensions by the way, a Firefox plugin is something like Java, or Flash…but these must be isolated as they run with much higher privileges than extensions. Firefox does use the sandbox design for extensions, but they don't use process boundaries to do so, they have a homegrown solution (see addendum two for an explanation using the apartment analogy) to make sure extensions can't read other extension's data, and each extension belongs to one owner tab…think of this like the dll design in Windows, and the equivalent performance gains you get from using in-process loading. I just don't understand Google's implementation of per-process extension loading, but I'm assuming the smart folks over in Mountain View have a good reason to do so. But, at the expense of overall performance and resource consumption.

Neither is a perfect solution, but memory on computers is still the scarcest resource on any machine, especially 32bit machines (such as the laptop I'm writing this post on). My desktop machine has 8 gigs, and my server has 16 gigs…and even that isn't enough these days. 4 gigs is like living in the stone age of computing, but I'm stuck with it on this machine. Chrome by itself can take up to 25% of that, so it's not an option for me. I wish both vendors would take a long hard look at how much the web itself has evolved since their respective browsers were released: It's a completely different web now, and web pages are consuming more and more memory. Be proactive in that regard. Also, get on top of the plugin developers about proper design (I'm not saying make the extension realm a police state like the iTunes App Store is, but at least I know the iPhone apps I install went through some sort of QA checks). And finally, just trim the fat from the browser itself. There is no reason why something as basic as a web browser needs to bring modern machines to their knees. Geeky addendum time.

 

Addendum One: Threading.

Threading is one of the holy grails of computing, and is also one of the most widely misunderstood and abused programming paradigms. Geek explanation first, then onwards with my apartment analogy to simplify it. A process can host multiple threads. A thread is a line of execution within a process: A thread carries out its set of logic independent from other concurrently running threads, though you can share data between threads. When you open a windows based application like Word, or a web browser, a process spins up (the .exe) and spawns a single main thread, usually the thread that hosts the window itself. A single threaded application only ever has one thread, and all lines of code execution run on this thread. This is undesirable, and I'm sure most of you have experienced why. When a thread executes an instruction set, it blocks all other requests until it's done (forcing other requests to queue up behind it). If you've ever clicked a button in an application, and the window freezes or says not responding, that's an example of thread blocking/queuing: The thread is busy, and when you click on it or try to drag the window around or minimize it, that request is queued until the thread can service the request.

Single threaded apps are rare, and most modern programming frameworks support multithreading. But, writing a properly working multi-threaded application is not a trivial task. Multi-threading works like this: The main thread can spawn multiple worker threads at will to carry out different lines of code execution. This solves the queuing problem for the most part as an application can get more work done at the same time, and blocking/queuing is drastically decreased. Getting all of these threads to play together nicely is where it gets beyond complicated, even for modern programming frameworks.

Let's take an extremely simplistic example before I move on to the apartment analogy. Suppose you have a very simple windows based application that allows you to calculate n number of primes and their values up to a specified number. The GUI is simple enough: a textbox that allows you to input the ceiling value, a button that says "Compute", and some sort of control that displays the values themselves. For small numbers like say, primes between 0 and 20, the result is instantaneous and will not block the main thread. But let's say you wanted the values of all primes between 0 and 1,000,000,000. This is going to take some time, no matter how tight your algorithm is. If this runs on a single thread, the application will be unusable until that routine has finished running. However, if you kick off the computation routine on a child thread, that thread will go do its work in the background, and the app is free to do other things, like find the factorial of a very large number, or do the Fibonacci sequence for 1,000,000 values (each of these on their own thread as well of course). But, these threads need to eventually give the result back to the main thread so it can display the value(s), so these threads must "merge" back into the main thread somehow, and they also have to let the main thread know when they are done. This can be accomplished in a couple of different ways:

  • Callbacks (also known as a semaphore): The child thread notifies the calling thread that it is finished and is ready to supply the result; it basically transfers control back to the calling thread, then terminates itself. The parent thread can either stop the work it is doing and process the result, it can continue its work and tell the child thread to wait until its done, or it can tell the child thread that it needs to wait for another child thread to finish its work first (in case results need to be processed in a specific order). This would be the equivalent of a child thread jumping up and down (just like real children) and saying "I'm done with my work, come check it out!"
  • Polling (also known as a spinlock): This is used in cases where the parent thread doesn't have much work to do, so it keeps polling child threads to see if they are done with their work. This model assumes the parent is done with its work and is waiting. It can either process the data as it comes in, or poll all child threads until they are all finished with their work, then move on. This is similar to an actual parent checking on a child who is supposed to be cleaning their room, or perhaps you've given multiple kids work to do (you, clean your room, and you do the dishes), and you can't run errands until they're done with their work.

There are other paradigms, but these two cover most cases. Where threading can get really nasty are two conditions that can occur:

  • Deadlocks: Two threads try to access the same data at the same time, with each thread waiting for the other thread to then release the value. Both are contending over the same data. Modern frameworks can assign weights to different threads, and after a set amount of time, the higher weighted thread wins. This is the equivalent of kids fighting over toys, then the parent stepping in and picking who wins (the calling thread in this case).
  • Race Conditions: A thread reads a data value that it needs to do a computation with…right before it does the calculation, another thread changes the value of the data, and the first thread then computes the wrong calculation. Realize that computers are very fast, all of this happens on the order of nano-seconds. Race bugs are some of the hardest bugs to squash in programming, which is why the notion of locks exist: You can lock a data structure which basically says "while I'm doing this, no other threads can access my data"...this would be like a child coloring a picture, then gets distracted by the TV…his sibling comes in and decides he likes the color blue better, and colors over the existing picture. The TV show ends, and the other sibling comes back and sees that it has changed colors. This can be prevented by the first child locking his bedroom door while he goes to watch his TV show.

Inter-child-thread communication is about as difficult as it sounds if we keep rolling with the child analogy. Two kids are pretty easy to maintain, especially if there are two parents in the house. But let's say you have 4 kids, or more…trying to get a set of tasks done on time, getting the kids to cooperate and share tasks (or even better, have them doing multiple tasks at one time, or switching tasks if they get bored with what they're doing), keeping them on task without them wondering off to watch TV, or get into fights with each other, and ideally they come and check in with parents when they are done so they can be told to do something else (see callbacks/polling above)…this is eerily similar to getting computing threads to play nice as well. Just like parenting, it is much more art than science. I won't go into detail here, but very strange stuff can happen when threads stop cooperating with each other.

Back to explaining threading in general, and why threading is a good thing if done correctly. Continuing on the apartment analogy: Something breaks in your apartment, and you need the office to fix it for you. If only one person worked in the office, and had to take care of everything by themselves…well, not much would get done in a timely manner. You head off to the office to consult with what is sure to be the most miserable property manager in the world, and discover a line (queue) of other tenants. The manager can only process one request at a time, and you have to wait your turn. At some point, he has to leave the office to actually get the work done  The single thread (manager) can only tend to one process (apartment) at a time, and it's very time consuming as the tenant can't cook, use the bathroom, wash her hands until the water issue is fixed (the thread is blocked until the work is done).

In a multithreaded design, there would be an office manager who directs multiple workers, each of whom can take service requests. The parent thread would be the manager, and the child threads would be the subordinates. If there is only one thread and it needs to process 8 requests, this is 4 times slower than 4 threads needing to process the same amount of work: 8 requests at 2 minutes each with one thread = 16 minutes. 8 requests at 2 minutes each with 4 threads = 4 minutes. Vastly simplified, but you get the point. The office manager then prioritizes (assigns a weight to) each work order based on severity, time to complete, and ideally gives the work best suited for each individual worker. Workers might need to cooperate on some jobs, and that can either go through the manager, or they can just call a co-worker. If the worker is busy, the calling worker can wait, or go do something else. Regardless, this is much more efficient provided the manager is good at what she does, and the workers are efficient.

Almost every new programmer I've met or trained, upon discovering threading, their faces light up and I know exactly what they are thinking: I'll just throw a bunch of threads into an application, compute the largest known prime number known to mankind, and get published in all the journals. If threads were the solution to everything, then it wouldn't be nearly as difficult to implement as it is. Here are the caveats (and they are huge):

  • Too many threads create more work and processing bottlenecks than they are worth. Threads are great up to a point (if you've made it this far, do yourself a favor no matter what industry you are in and read the book entitled "The Mythical Man-Month"…it's geared towards the software industry, but can be applied to virtually anything you want to apply it to. When you write multi-threaded applications, it's up to the operating system to maintain the threads via the Thread Scheduler (also known as a context-switcher). In a nutshell, based on parameters you specify in your code, the thread scheduler then decides how much time to give each thread, maintains the threads themselves, takes care of merging, joining, data exchange, and which thread should run at what time and for how long of a duration. These are all called context switches, and can actually decrease performance if too many threads are used since the work gets too chunked up, and the context switches themselves aren't free, they incur overhead.
  • So, how many threads should I use then? Until relatively recently, consumer machines had a single processor, with a single core. Multithreading has been around since the early days of computing, but on single processor machines, it was an illusion. Apps seemed multithreaded, and OS's seemed to be able to run more than one task at a time, but what was happening under the hood is that the scheduler was simply switching between processes rapidly (again we're talking nanoseconds here, not perceptible to humans) to give the illusion of threads: This is called multitasking (and is similar to how we use that word in the human world, doing more than one thing at a time, and placing priorities on each task). To only give a processor a single task would be a huge waste of resources as computers are very good at one thing: Doing tasks extremely fast.
  • Multiprocessor systems are the de-facto standard now, as are hyper-threaded processors. My desktop machine has a dual core hyper-threaded processor, that's 2 physical cores, and 4 logical cores. My laptop is a quad core hyper-threaded setup, so 4 physical cores and 8 logical cores. One thread can be handled by one core, so an application could run 8 threads concurrently with no problems at all on my laptop, without the need to multitask. There is no hard fast rule, but generally speaking it shouldn't be any less than the number of logical cores on the user's machine. All of these environmental factors are discoverable at runtime as you won't know what configuration the user is running, and numerous libraries exist to ease the burden of threading. When you factor in multi-tasking, and thread pooling (and lots of testing) you can nail down a firm number. Most consumer apps won't need more than 2x the number of logical cores though.

So why not just through a gazillion threads at a complex computation? Back to our apartment manager. She thinks to herself "well, I can get more work done if I hire more workers, and our tenants will be quite happy when their issues are addressed in a more timely manner." So, she goes out and hires a slew of new workers, let's say she hires 20 new workers to bring the total number up to 24. This creates several problems without really solving any:

  • If there isn't enough workload to keep each worker busy, she's losing money by paying them to do nothing. Same thing with unused threads, they are sitting around consuming computer resources with no benefit to the system (these kinds of factors weigh heavily into systems design by the way: Too many times I've seen managers go out and build a massive server with terabytes of storage, 100's of gigabytes of RAM, and a processor that's so fast it can travel forward in time and compute a result set before the user even knows they need it. The system is deployed, and the processors peak at 10% usage, the app only consumes a few gigs of memory, and the database barely tips the scales at maybe 20 gigs in size. All of those extra resources are sitting around doing nothing. This is especially common in the power-user consumer space. Go to any forum geared towards pseudo-computer geeks. You want resource usage to be at about 80% of capacity at all times in business systems, and consumers almost always buy more computer than they'll ever need, 95% of the time your machine is sitting there doing nothing at all).
  • The flipside is trying to manage all of these workers. Soon she finds herself in the managing workers business, and not the fixing/renting/marketing apartments business. She's swamped doling out work orders, figuring out scheduling, holding meetings, inter-staff conflicts, workers calling in sick, workers getting work orders wrong, ad nauseam…in the end, she suffers as do her tenants. Just like in the business world where trying to figure out the right balance of number of workers vs. tenant satisfaction is a balancing act, the same goes with threading and is the equivalent to wasted resources and context switching.

All of these are vastly simplified, but should give a general idea as to how threads work, and the pitfalls that can crop up, and how it relates to the initial topic of web browsers and resource consumption.

Addendum Two: Alternatives to processes

This will be a much shorter addendum, and the apartment metaphor will be a bit more vague, but this topic is worth mentioning: There are alternatives to threading (though they do still need the threading infrastructure, what they don't need are separate processes to implement the isolation they need). As stated in the core of this article, modern browsers run different aspects of the browser in separate processes: They do this for isolation, security, and stability, but at the expense of cross boundary performance issues, memory consumption, and complexity. There are two alternatives in the Windows space to spawning individual processes, and both of them piggy back on the threading paradigm mentioned in addendum 1. As I've already been over threading, I'll compare these two to processes.

We've already covered just how slow and expensive processes are, which begs the question "is that are only choice?" No. I know of two fairly well known alternatives (and no doubt there are more, but these are part of Windows itself so why reinvent the wheel). They are:

  • Fibers. Though now fairly obscure, this was Microsoft's attempt at implementing lightweight processes that exist within threads themselves. I'm the first to admit that I don't know much about Fibers, other than they attempted to implement the benefits of processes, without all the overhead. A fiber is like a process within a process (actually a process within a thread). I'll leave it at that, curious readers should consult Google for more information.
  • .Net has the notion of Application Domains. A few resources before I continue:
  • .Net AppDomains are similar to fibers in purpose, but different in several ways. First off, they can only be used the .Net framework, and languages that adhere to the CLI standard. Secondly, processes host threads, which host fibers, whereas a process hosts AppDomains, which hosts threads. As stated before numerous times, processes are very expensive. AppDomains are not, but come with all the benefits of processes, and AppDomains can host dll's as well. A short list of what AppDomains do:
    • Multiple threads can exist within a single application domain.
    • An application within a domain can be stopped without affecting the state of another domain in the same process.
    • A fault or exception in one domain does not affect an application in another domain or crash the entire process that hosts the domains.
    • Configuration information is part of a domain's scope, not the scope of the process.
    • Each domain can be assigned different security access levels.
    • Code in one domain cannot directly access code in another.
  • AppDomains can communicate with other AppDomains running in the same process while still guaranteeing the above bullets via a process called Marshaling, which is much faster and more efficient than a cross boundary process call. For even cheaper cross domain calls, you can create a ContextBound object which can be passed by reference (a managed code pointer) rather than the deep copy by value passing that Marshaling uses. I wrote an article on ContextBound objects 7 years ago if you'd like a more in depth discussion.

So of course the logical question is "why not just use one of these two alternatives on Windows?" The first answer lies within the question itself: These two technologies are tied to Windows only. Chrome and Firefox are cross-platform: They run on Windows, Linux, and Mac OS X. As such, the majority of the code needs to be portable, otherwise it becomes a maintenance nightmare. Second: Fibers are largely deprecated (not officially, but no one uses them, and they are poorly documented), and you need to write your own scheduler. Finally, AppDomains are available only in .Net, and languages that fully implement the CLI. So why not just write a web browser in .Net? It's Windows only for starters (though Mono (the open source implementation of the the ECMA .Net standard) is supported on numerous platforms, but not fully baked yet). And, .Net is not suited to writing something as complicated as a web browser: Performance would be beyond awful. I am not downing .Net, this just wouldn't be its forte.

My point is that I believe there are better solutions out there, or one that could be hand rolled and implemented within the browser itself. Throwing a bunch of processes at a problem is worse than over-threading in my opinion. With one of the paradigms above, you could have a single parent process that hosts multiple app domains, which in turn load the dll's they need to get their work done: If an app domain fails, it won't bring down other app domains or the parent process with it, all you lost are the threads and the child dll's, and app domains are very cheap to respawn. Implementing something similar to the above constructs could shave off some serious bloat from the browser and its extensions (unfortunately, we cannot control how much memory a web page consumes when it is rendered, that's up to the developers themselves).

This is where my apartment analogy starts to break down a little bit (and I'm only going to use app domains), but I'll give it a shot. Our apartment manager is pulling her hair out trying to manage all these workers (threads) and needs help. Too much work to be done for tenants, but managing all these workers is bringing down the core businesses: Satisfying tenants, renting apartments, and marketing to would be tenants. This isn't going to turn out well, but she doesn't have the option of firing workers since there is plenty of work that needs to be done. She decides to add a buffer layer between her and these workers, and hires 4 supervisors to oversee 6 workers each. Each of these supervisors provides an isolation boundary between their department and the other ones. Our manager (the process) now manages 4 supervisors (the appdomains), who in turn manage 6 workers (threads) of their own. Of course, intradepartmental cooperation will need to occur, but instead of the workers just calling any co-worker to help them out, they have to go talk to their supervisor, who can then call another supervisor to see what his workers are doing, and get the resources they need scheduled (this is Marshaling between app domains). Also, if one supervisor's workers screw something up, it doesn't affect another supervisor's workers (isolation boundary implemented by the app domain), and workers need to go through their supervisor to get work scheduled from another department (thread pool). So here's our finalized workflow:

  • A tenant (a separate process) calls the office manager to get a work order created.
  • The manager creates work orders and starts assigning them to the supervisors (app domains). She doesn't need to be concerned with the details like scheduling, allocation, checking for completion of work…this is all handled by the supervisors now, and she can focus on the actual business end of her job.
  • The supervisor organizes her workers (threads) and puts them to work, checking in occasionally (polling) and waiting to obtain the results of the work order (callback).
  • She can then let the manager know what resources she has available and waits for more work to be assigned.
  • Individual workers have to go through their supervisor first if they need extra help getting a task completed (marshaling): They are oblivious to other threads in different app domains, as they should be. Their supervisor manages joint projects (thread merging and joining).

So in the case of our fictitious apartment complex, more work gets done in a timely fashion, resources are utilized more efficiently, and each individual can focus on their own work without getting bogged down with tedious tasks which interfere with the business.

Would this be difficult to implement initially in a web browser? Yes, but I believe this design could really benefit most applications, not just web browsers…browsers are just easy to pick on because A) everyone who has internet uses them and B) it provides a better definition of the chain of responsibility for components and subsystems.