DocShell/Fastback
Fastback, or back-forward cache, is a mechanism to save nsIContentViewer objects (which include the DOM and PresShell/frame tree) in a session history entry, and restore it if the page is reloaded from session history. It can be broken down into saving to history, restoring from history, and evicting from history. This document tries to provide an overview of how different parts of the process fit together. It doesn't attempt to describe each function in line-by-line detail; the code and comments are the best reference for that.
Saving to history
When the docshell is about to replace a ContentViewer with a new ContentViewer, it first determines whether the old content viewer is a candidate for saving in session history. This work happens in nsDocShell::CanSavePresentation()
. There are a number of factors which might disqualify a particular viewer from being saved, for example, the type of load being performed, whether the page was finished loading, or an unload event handler being installed. We check this state once before initiating the new document load, and again once we're ready to actually swap out the content viewers. The presentation is only cached if both checks pass.
If we determine that the content viewer can safely be cached, then we need to capture some extra state and "freeze" the presentation. Both of these happen through nsDocShell::CaptureState()
. SaveWindowState()
is called on the window object, which saves the focus state and suspends timeouts. CaptureState
also suspends refresh URIs, captures the size at which the presentation was laid out, and saves off the list of child docshells. The latter is important since the parent docshell will clear its child list when it is switched over to the new document.
The final part of saving to history happens when DocumentViewerImpl::Close()
is called on the old content viewer, and Show()
is called on the new one. Close
stores a reference to the session history entry which will contain the DocumentViewer (this comes from the DocShell's mOSHE). When the new viewer is actually ready to paint, Show
is called and this calls Destroy
on the previous viewer. At this time, rather than tearing down the presentation, the document viewer removes itself from the view hierarchy to suppress painting, and saves a reference to itself into the history entry. The history entry now has sole ownership of the DocumentViewer. Also at this point, the document's link to its docshell/window is broken, so that the document is completely detached and cannot affect the window which is now showing a different document. Plugins in the document are destroyed, since there's no reliable way to suspend a plugin. This is the end of the save-to-history.
Restoration from history
The restore process is mostly just a reversal of the saving process. nsDocShell::InternalLoad
checks whether the load is a history load and if so, whether it has a cached presentation. If it does, it calls nsDocShell::RestorePresentation
. The major chunk of work here is simulating a load so that the progress notifications happen in a reasonable way, which is to say, as close to a normal load as possible. Because of this, chunks of code from Embed()
and CreateContentViewer()
are adapted into RestoreFromHistory
. Once the new content viewer is set up, the document's channel is readded to the docshell's load group, which causes the required STATE_START
notification. We then process subframes recursively, adding them to the loadgroup. To complete the notifications, the channels are then removed from the loadgroup, depth-first. We restart timeouts, refresh URIs, and plugins, and we're done.
It's possible that the size of the docshell has changed since the presentation was stored. In this case, we need to resize the root view's widget to match the new size. This will cause the necessary reflows and repainting to occur.
Eviction from history
The number of content viewers in session history is limited based on the physical memory on the system. Currently, there is a fixed limit which is a logarithmic function of the memory size, with a maximum of 8 cached viewers.
On top of this global limit, we limit each nsSHistory instance (which corresponds to each browser tab) to a maximum of 3 cached content viewers. This avoids wasting memory for viewers which the user is very unlikely to revisit.
Once a new content viewer has been placed into session history through a call to DocumentViewerImpl::Destroy()
, nsSHistory::EvictContentViewers()
is called. It first checks the limit for the current nsSHistory object, by determining a sliding window of SHEntrry indices which are allowed to have cached viewers. The sliding window has an endpoint at the new current index, and the other endpoint is 3 back (if we're going forward) or 3 in front (if we're going backward). Any viewers outside of this range are evicted. Next, it checks against the global limit, by computing the sliding window for each nsSHistory. The viewer which is the furthest away from its SHistory's current index is considered the best eviction candidate, because it's the least recently used across the app. At the end of this process, we have a count of all cached content viewers. If it's more than the fixed limit, we evict the one that was furthest away. If we still need to evict viewers to reach the limit, we start the entire process again.
This algorithm is inefficient (O(n²)) in the worst case, but O(n) in the common case, since we'll rarely need to evict more than one viewer -- only if the user manually changes the pref that controls the limit. There is a fast path for "clear history" which evicts all cached viewers in O(n) time.
The global limit can be overridden with the browser.sessionhistory.max_total_viewers
pref. The default value of -1 computes a limit based on physical memory as described above. Any other value acts as the global limit, with 0 disabling caching entirely. The per-SHistory limit is defined by the gHistoryMaxViewers
constant.