This document is a quick overview of the Blink implementation of IndexedDB read/write requests.
Chrome's IndexedDB implementation is logically split into two components.
The two components are currently (Q4 2017) hosted in separate processes and bridged by a couple of glue layers. As part of the OnionSoup 2.0 effort, we hope to most of the backing store implementation in Blink, and remove the glue layers.
The backing store implementation is built on top of two storage systems:
Storing a JavaScript value in IndexedDB is specified at a high level in the HTML Structured Data Specification. Blink‘s implementation of the specification is responsible for converting between V8 values and the byte sequences in IndexedDB’s backing store. The implementation is in SerializedScriptValue
(SSV), which delegates to v8::ValueSerializer
and v8::ValueDeserializer
. A serialized value handled by the backing store is essentially a data buffer that stores a sequence of bytes, and a list (technically, an ordered set) of Blobs.
While V8 drives the serialization process, Blink implements the serialization of objects not covered by the JavaScript specification, such as Blob
and ImageData
. This is accomplished by having V8 expose the interfaces v8::ValueSerializer::Delegate
and v8::ValueDeserializer::Delegate
, which are implemented by Blink. The canonical example methods of these interfaces are v8::ValueSerializer::Delegate::WriteHostObject()
and v8::ValueDeserializer::Delegate::ReadHostObject()
, which are used to completely delegate the serialization of a V8 object to Blink.
Changes to the IndexedDB serialization format are delicate because our backing store does not have any form of data migration. Once written to the backing store, an IndexedDB value's format will never change. It follows that the SerializedScriptValue implementation must be able to read serialized values written by all previous versions of Chrome. To avoid data corruption, the SSV implementation should also detect (and reject) serialized values written by future Chrome versions, which can happen when a user downgrades the browser (e.g., by switching channels from beta to stable) and when serialization changes are reverted. For the reasons above, technical debt introduced by unnecessary complexity in the serialization format is much more difficult to pay than in most of the Chrome codebase.
IndexedDB serialization changes must take the following subtleties into account:
Small IndexedDB values (whose serialized size below 64KB) are stored directly in the backing store.
All the IndexedDB write operations (put, add, and update) are currently (Q4 2017) routed through an IDBObjectStore::put
overload.
All IndexedDB requests, including read/write operations, are translated by the Blink side into lower-level requests, then sent via Mojo IPC to the browser process, where they are executed by the backing store. Most of the data associated with an IndexedDB write operation is transferred from the renderer to the browser using one Mojo call, and is therefore subject to the Mojo message limit. Blobs are an exception, as they are transferred to the browser process by the Blob subsystem.
The Web platform has a simple, synchronous API for creating a Blob, which can be used in one line of code. Conversely, reading a Blob's content is an asynchronous process that requires creating an intermediate FileReader instance, and setting up a handler for its loadend event. This is not an accident. When a Blob is constructed, all the information needed to build its content is available in the renderer calling the constructor. Once constructed, a Blob instance only stores a handle to the content -- for example, most Blobs in Chrome point to on-disk files. This is the core reason behind the significant complexity gap between IndexedDB value wrapping (write-side changes) and unwrapping (read-side changes).
An IndexedDB read operation, like IDBObjectStore.get, creates an IDBRequest that tracks the status of the operation. Blink's IDBRequest
implementation creates a WebIDBCallbacks
instance, and passes the request and the WebIDBCallbacks to the browser-side IndexedDB API.
The browser-side IndexedDB implementation executes requests from the Blink side in a single-threaded loop, and relies on Mojo to queue incoming requests. The IndexedDB backing store retrieves the desired value(s). Each IndexedDBValue contains the SSV data (treated as an opaque sequence of bits, on the browser-side) and a vector of Blob handles.
The result of each read operation is sent from the browser process to the renderer process via a callback (a Mojo call to an interface associated with the database receiving the request). In the renderer process, the result is converted to a WebIDBValue
and passed to the WebIDBCallbacks
instance, which further passses it on to the corresponding IDBRequest. The IDBRequest updates the Blink-side IndexedDB state, attaches the IDBValue
result to the IDBRequest, creates a DOM event representing the result and queues the event.
The Blink-side result processing has a few subtleties that are relevant to this design.
ExecutionContext
used to dispatch DOM events may be suspended, which happens when the user creates a JavaScript breakpoint in DevTools, and the breakpoint is hit. At the time of this writing (Q4 2017), each Blink feature deals with suspended execution contexts individually. In most cases (think input events), the simple strategy of dropping the events on the floor is acceptable. Unfortunately, this is not acceptable for IndexedDB (the specification demands that each request gets a result or an error), so IDBRequest events must be queued and dispatched in-order when the ExecutionContext is resumed.Blink wraps large IndexedDB values in Blobs before sending them to the browser's LevelDB-based backing store. The large value threshold (serialized value size at or above 64KB, as of Q4 2017) takes the following factors into account:
Blobs that contain SSV data use the MIME type application/vnd.blink-idb-value-wrapper. In order to be as user-friendly as possible (for the unlikely event that a developer is exposed to a Blob wrapping an SSV data buffer), the MIME type was chosen to be easily searchable and fairly self-explanatory, and was registered with IANA.
IDBValueWrapper
contains all the logic for serializing an IndexedDB value via SerializedScriptValue
. IDBObjectStore::put
passes the V8 value into IDBValueWrapper, and gets back the SSV data that is passed to the browser-side IndexedDB implementation. When given a large IndexedDB value, IDBValueWrapper
creates a Blob that holds the serialized value, and stores a reference to that Blob in the IndexedDB backing store.
Large IndexedDB values are unwrapped in Blink using a fairly close emulation to the process used by a Web application to read the contents of a Blob stored inside an IndexedDB value, so it is instructive to understand what happens in that case.
blink::BlobDataHandle
.FileReader
implementation uses a FileReaderLoader
to retrieve the Blob’s content from the Blob system in the browser process.DidFinishLoading
is called, which eventually causes the FileReader to queue an onload event.The IndexedDB read path uses classes below to detect and unwrap Blob-wrapped IDBValues. Reading Blob contents must be asynchronous, because Blobs can be disk-backed. In fact, all Blobs coming from IndexedDB are currently (Q4 2017) disk-backed.
IDBValueUnwrapper
knows how to decode the serialization format used by wrapped data markers. It can tell whether an IDBValue contains a wrapped data marker and, if so, it can extract a BlobDataHandler pointing to the Blob that contains the wrapped SSV data.IDBRequestLoader
coordinates a FileReaderLoader and an IDBValueUnwrapper to map an array of IDBValues that may contain wrapped SSV data into IDBValues that are guaranteed to be unwrapped. IDBRequestLoader operates on an array of values because some requests, like IDBObjectStore::getAll return an array of results. Single-result requests, like IDBObjectStore::get are handled by wrapping the result in a one-element array.IDBRequestQueueItem
holds on to an IDBRequest for which Blink has received an IDBValue from the browser process, but hasn't queued up a corresponding event in the DOMWindow event queue.IDBValue unwrapping relies on the following data in existing IndexedDB objects.
IDBTransaction
owns a queue of IDBRequestQueueItems, where the queue ordering reflects the order in which the requests were issued by the Web application.IDBRequest
exposes HandleResponse
methods (overloaded to account for different response types), in addition to EnqueueResponse
methods. WebIDBCallbacks calls into a HandleResponse method, which handles SSV unwrapping and queueing. EnqueueResponse is responsible for updating the IDBRequest's status (e.g., its result property) and enqueueing a DOM event in the appropriate queue.Reading large values follows a slightly more complex process than reading small values. For simplicity, we describe the single-IDBValue case. Extending the logic to an IDBValue array is fairly straightforward.