cookie-split-loading
Currently, the CookieMonster cookie store is initialized by reading in the cookies from the backing store in a large lump on first access. This is bad for several reasons:
- It violates the principle that we shouldn't do blocking operations on the IO thread.
- It contributes to perceived user startup time in if a session is being restored (which requires URL accesses, which result in CookieMonster accesses).
- It contributes to variation in web page loading, as the first web page takes substantially longer than later ones.
This problem is tracked in Issue 52909. This document sketches out a proposed solution to this problem.
Requirements
- Minimize startup time (including startup time which includes session restore, i.e. CM access).
- Minimize stalling of individual set/get cookies.
I believe that these requirements imply:
- Basic startup cannot be gated on loading all cookies from persistent backing store.
- Individual page loads cannot be gated on loading the entire cookie store from persistent backing store. Ideally, individual page loads would only load the cookies from persistent backing store that are needed for that page.
- Full persistent store load should be initiated at startup, but in the background not blocking any processes (as having cookies loaded in memory will result in absolute minimum latency).
Notes on synchronous vs. asynchronous interfaces to the CookieMonster
(See Issue 68657)
Generally, services on the IO thread provide asynchronous interfaces with callbacks, to avoid the IO thread becoming a bottleneck in providing services for other threads. For all requests after the initial one, the CookieMonster follows this rule and does not block; all information required for all CookieMonster APIs is in memory. However, the first call to the CookieMonster triggers loading in of the entire persistent backing store, done on-thread, which means that the IO thread blocks on disk operations on the first request to a CookieMonster with a backing store.
Giving the CookieMonster an asynchronous interface is a large task, not because of the internal structure of the Cookie, but because of the large number of callers, all of whom would need to be adapted to the new interface. Issue <xxx> tracks this task.
In an ideal world, the current design would not be dependent on async CookieMonster interfaces. However, a key part of this design is breaking up the loading of the backing store into indepedent pieces, using Task objects, which are spawned off asynchronously in the backing in the absence of incoming reuqests to laod the backing store database. Combining this with a synchronous interface for those reuqests that need backing store loads would mean doing one of:
- Loading data inline (in the IO thread) when it is needed for a particular reuqest. This would probably mean that all load tasks would need to be on the IO thread.
- Completely blocking any IO thread activity while waiting for responses from the DB thread for data load-in. This violates one of the basic principles of the IO thread (though in theory it's no worse than what we're doing now).
- Running nested message loops, and carefully tracking which requests corresponded to which nesting level of the message loop. If request B were run while request A had an outstanding load message to the DB thread, this means that request A would be blocked on the completion of request B (because the stack couldn't unwind until that point).
None of these options is particularly appealing. Thus it's preferred that this work be done after the CookieMonster has been converted over to having an async interface. The rest of this design assumes such an interface.
[Note that it would be relatively easy to layer an asynchronous interface (if not behavior) on top of the current synchronous interface; the async trigger would wrap the sync call and a PostTask(self) for the callback. This could allow incremental changes to the various callers. When all callers had been converted, the sync interface could be removed and the async interface made to behave in a non-blocking manner. ]
High Level Design
The cookie monster will get an additional data structure, indexed by domain key (currently eTLD+1) indicating whether or not a particular domain key's set of values has been loaded. Incoming requests (either set of get) will trigger the loading of the domain key(s) required for those requests if they have not yet been loaded. In these cases, the incoming request will block (as it currently does if the backing store hasn't been loaded). All actual database accesses will occur on the DB thread.
At CookieMonster construction, a background operation will be invoked to load the entire cookie store. This will run on the database thread, and will break down into a series of tasks (T) marks each separate task:
- (T) Get the list of domain keys contained in the persistent backing store.
- For each key:
- (T) Load all data from that key into memory
- (T -- IO thread) Incorporate that loaded data into the cookie monster and mark that key as loaded.
These are broken up into indiviudal tasks both to avoid freezing the DB thread, and to allow loads of specific keys triggered by particular web page access to "jump the queue" and get pulled in earlier.
Routine Flow Outline
The primary routines and data structures involved in this design are sketched out below.
There are two basic "ropes" (since they're potentially cross-thread) of routine invocation: the initial spawn of loading of all of the cookies, and the load-on-demand triggerred by specific requests to the cookie monster.
The initial spawn occurs from the CookieMonster constructor -> SQLitePersistentCookieStore::SpawnFullLoad -> SQLitePersistentCookieStore::BackingStore::SpawnFullLoad. Once the rope has reached the DB thread, a full list of all the keys in the SQLite DB is loaded and stored in BackingStore. Then SpawnFullLoad does a self-dispatch (to the DB thread) of a ChainLoadKeyCookies task for the first key in the list. ChainLoadKeyCookies will call LoadKeyCookies (which will load in the cookies associated with that key from the backing store and notify the CookieMonster of that load) and will then self-dispatch another instance of ChainLoadKeyCookies. When the last entry in the key list is loaded, ChainLoadKeyCookies will return without any action. This will (incrementally, allowing other tasks to run on the DB thread) load all the cookies.
If a request comes into the CookieMonster while this chain is executing, it will check all_loaded_ and keys_loaded_ to detemrine that the relevant data for the request has not been loaded (if the relevant data has already been loaded, it will execute the in-memory lookup inline as current and call the callback with the information). It will then register the callback and operation information into the key->callback mapping. If that is the first registration of that key, it will call SQLitePersistentCookieStore::LoadKeyCookies, which will dispatch an invocation to the DB thread of SQLitePersistentCookieStore::BackingStore::LoadKeyCookies. If LoadKeyCookies determines that the key requested has already been loaded (race with above chain), it will simply exit.
The tricky part of this design is making sure that there are no races that result in dangling requests. The invariants that guarantee that this doesn't happen are:
- The first entry in the keys->callback table is always correlated with a LoadKeyCookies dispatch to the DB thread.
- That entry/dispatch will only happen if no previous CompleteLoad for that key has already arrived.
- The only case in which a LoadKeyCookies dispatch will not result in a later CompleteLoad dispatch is if a previous CompleteLoad dispatch for that cookie has already been sent.
So it's possible for a LoadKeyCookies() dispatch to not result in a CompleteLoad() being sent back, but only if there's already one in transit. Thus, if a callback is registered in the keys->callback table, it will be executed with maximum latency round trip to DB thread + queue wait time + load time.
Object Lifetime Relationships
[Will Chan take note :-}.]
Both CookieMonster and PersistentCookieStore are RefCountedThreadSafe objects. The CookieMonster keeps a reference to the PersistentCookieStore in its store_ pointer; this guarantees that the store lives as long as the Monster does.
Both SpawnFullLoad and LoadKeyCookies will take as arguments scoped_refptrs on the CookieMonster which the PersistentCookieStore (or its backing store) will use for callbacks. These references will guarantee that the CookieMonster stays alive for the duration of the callback.
If the CookieMonster needs to be torn down, it will call an abort() routine on the PersistentCookieStore, which will break the chain load and release the scoped_refptr<> held on the CookieMonster by that chain load.