Backing Store

DataNav currently employs a simple file system-based strategy for storing portal content. Portal structural and state information are stored in JavaScript Object Notation (JSON) files, archived figures and hub navigation view template figures are persisted as conventional FypML figure files, and hub data sets and "metadata" for both data and figure archives are saved in custom-formatted binary files. All files specific to a particular archive are kept in a subdirectory assigned to that archive. The entire collection of files and per-archive directories is known as the archive vault backing store. Every DataNav portal has a vault backing store residing on the server's file system, along with some additional metadata files that persist the registered users list and the vault modification information. Furthermore, DataNav Builder uses the same backing store infrastructure to manage the user's private collection of archives within his DataNav workspace on the host machine's file system.

Backing store directory structure

Here is an excerpt from a directory listing that exemplifies the layout of the directory structure for an archive vault backing store. Let $STORE represent the backing store's root directory on the file system. 

$STORE/

   contents.json

   store.lock

   transaction.txt

   hub_43891668/

      hub.dnc

      data.dhr

      view_55189743930.fyp

      view_21699823511.fyp

      ...

   figarch_378122956401/

      figarch.dnf

      fig_20058230.fyp

      ...

Observe that there is a subdirectory for each data or figure archive defined on the portal. The subdirectory name has the form hub_{UID} or figarch_{UID}, where the prefix indicates the archive type and {UID} is a 32-bit strictly positive integer that uniquely identifies the archive. All backing store files specific to a given archive are kept in its archive directory. There are also three mandatory files in the backing store's root directory:

contents.json - The archive vault contents file. This file persists information on the list of archives defined on the portal. It contains a single JSON object with three fields: entity is a string identifying the file as a vault contents file; version is an integer specifying the file version number, for future migration purposes (current version = 4); and archives is a JSON array holding the vault's archive list. It is a mixed array of integers and strings: [uid1, type1, pub1, ..., uidN, typeN, pubN], where each triplet {uid, type, pub} defines the unique integer identifier (UID) , archive type ("data" or "figure"), and private/public flag (zero = private, non-zero = public) for each archive in the vault. The order of the archives within the list is the order in which archives are presented in DataNav Builder or Viewer. Each archive subdirectory represents the backing store for a single archive. For data hubs, this directory contains the hub content repository file (hub.dnc), the data repository file (data.dhr), and one view template file for each navigation view defined on the hub. Each template file view_{VUID}.fyp is a standard DataNav FypML file defining the figure template for the navigation view identified by the unique integer identifier {VUID}. These figure templates may be generated using Figure Composer and then attached to the hub view later via DataNav Builder, or they may be designed using a Figure Composer-like component within DataNav Builder itself.

The hub contents file persists all hub content except for the raw data sets and the view templates. It is a binary file especially formatted for random-access in an effort to minimize the amount of file I/O that must be done to put, get, or remove information from it. Listed below are the different kinds of structural and metadata content currently persisted in the file: 

Hub data sets referenced in the navigation view instance list are stored -- indexed by their UIDs for fast retrieval -- in the data repository file, a custom-formatted binary file that can grow to accommodate an indefinite number of sets. The file format optimizes the speed with which raw data can be added, retrieved, and removed from the repository. As sets are added and removed, the file can become fragmented, but it can be compacted to minimize wasted space. Compaction, of course, is a slow operation, and it is only performed as part of some larger, long-running task -- such as a hub upload or export operation.

The backing store for a figure archive contains a single metadata file, plus a standard FypML markup file for each figure in the archive. The metadata file figarch.dnf is to a figure archive what hub.dnc is to a data archive. It stores the archive's title, author list, and HTML-formatted description; the ordered list of UIDs identifying the figures comprising the archive, plus a legend (title plus description) for each figure. The name of each archived figure file takes the form fig_{FUID}.fyp, where {FUID} is the figure's UID.

What follows are detailed specifications of the binary file formats for figarch.dnf, hub.dnc, and data.dhr, for the benefit of interested developers and portal administrators. Other users will prefer to skip these sections entirely!

File format description: hub.dnc

The hub contents file begins with an 8-byte header, followed by a 2KB index block. These two sections are present even if no information is stored in the file. Obviously, this is wasteful, but it dramatically improves the random-access retrieval, revision, and removal of any content stored therein. The first allocated block in the file immediately follows the index block.

The header serves to identify the file as a DataNav hub contents repository. It is structured as follows:

The index block lists the type, size, and location of each allocated file block. One 2KB index block has room to store information on 170 file blocks. As the hub content grows and changes, additional index blocks are appended to the file as necessary, and the list of index blocks are joined in "linked list" fashion, with each index block including a "pointer" (byte offset from start of file) to the next block in the list. Each index block has a 8-byte header and 170 12-byte allocation slots. The header contains two 32-bit integers: the first is the file offset to the next index block (0 if there is none), and the second is reserved (0). Each allocation slot is structured as follows:

All hub content is stored in file blocks that are appended to the file as needed, with their size, location and type recorded in the next available allocation slot within the linked list of index blocks in the file. As content changes, it may outgrow the block in which it is stored; in this case, a larger block is allocated to hold the content, and the old block is marked as unoccupied in the index, so that it can be reused when new content is added to the file. If content is removed from the file (e.g., when deleting a navigation view), the blocks in which that content was stored are again marked as unoccupied in the index.

When a block is marked as unoccupied, the index is checked to see if the block that is physically before or after it is also unoccupied. If so, the two blocks are coalesced into one, which reintroduces an unallocated slot in the index. Furthermore, if the just-unoccupied block is at EOF, then the file can be truncated to reclaim that space -- again reintroducing an unallocated slot in the index. To coalesce or truncate unoccupied blocks as quickly as possible, all index blocks in the content file are parsed when it is loaded, and the index is cached in memory.

Because of the linked-list index block structure, and because adjacent unoccupied blocks are coalesced whenever possible, the order of the allocation slots in the index will NOT generally reflect the physical order of the allocated blocks in the file. Given a block at offset D with size S, the block after it will have offset D+S, while the block before it will have offset F and size K such that F+K = D. Additional index blocks will introduce "holes" in the file, effectively splitting it into different allocation sections. Blocks in different sections cannot be coalesced, since they can never be physically adjacent. However, if the number of allocated blocks decreases such that an index block can be reclaimed, then the index is consolidated over the remaining index blocks, and the reclaimed index block is marked as unoccupied. This is likely to be a very rare event: A typical hub is unlikely to need more than the 170 different content blocks that can be recorded in the single index block at the head of the file.

The content and structure of an occupied file block varies with the block type:

File format description: data.dhr

The hub data repository is a single binary file in which every data set in a DataNav hub is stored. It is formatted for random access to minimize the amount of file IO that must be done to add/retrieve an individual data set to/from the file. In addition to storing hub data, the repository also maintains a "data-to-view lookup table" persisting associations between data sets and the hub navigation views in which those sets are displayed.

A hashing scheme is crucial to the data repository file design. A 32-bit integer hash code is computed for every data set added to the file, and this code is XOR-folded into a 12-bit "bucket" index. Assuming the hash is a good one, the idea is to disperse stored data sets uniformly among 4096 buckets. The implementation allows for up to 65536 data sets per bucket, so the file may theoretically store over 268 million data sets all told. We anticipate that a given DataNav hub will contain FAR fewer sets than this!

Each data set added to the repository is assigned a unique 32-bit integer ID, or "DUID". This DUID uniquely identifies the data set within the hub and, more importantly, provides the means to quickly retrieve the stored data from the repository. The data set's bucket index forms the first 12-bits of the UID, while its index within the bucket forms the next 16 bits (the upper 4 bits are always 0).

For each non-empty bucket there are one or more file blocks called "bucket blocks" that store three pieces of information for each data set in that bucket: the set's 32-bit hash code, an "associated views" field that indicates the hub navigation views in which the data set appears, and the 8-byte file offset to the file block in which the actual data is stored. Storing the hash code provides a means of quickly detecting whether or not a new data set being added to the file is actually an exact duplicate of an existing set. Given the manner in which hub views are "populated" with instance data (the same data set could appear in multiple instances), avoiding duplicate data sets is fundamental to the implementation of the data repository.

The "associated view" field is a bit field of length 64 that makes it possible to associate each data set with up to 64 distinct hub navigation views. A fixed 64x4-byte "view table" near the beginning of the repository file stores the UID of the view corresponding to each bit flag in this field. There were several design considerations that led to this scheme:

The data repository file begins with a 16-byte header, which includes the file tag and some integer fields:

After the header comes the 64x4-byte "view table". Each 4-byte integer represents the UID of an existing hub navigation view; unoccupied entries in the table are set to 0, which is not a valid VUID. Index position in the table corresponds to the bit position in the 64-bit field in each data set's bucket slot. The table and the per-set "associated views" bit field allow one to attach each data set to any subset of 64 distinct views. An association is formed by storing the VUID in the view table (if it's not already there), then setting the bit at the corresponding position in the data set's bit field.

Immediately following the view table is the 4096x8 byte "bucket table", which is always present and is fixed in size. It is essentially a list of file pointers -- more precisely, 64-bit integers specifying the byte offset from the beginning of the file -- to the start of the first bucket block for that data bucket. When the file is first created and contains no data sets, all of the buckets are empty (no bucket blocks exist) and all of the bucket table pointers are 0 to indicate this fact. When the first data set is added to a bucket, an initial bucket block is added to the file and its offset is entered into the corresponding position in the bucket table. Observe that, once you know the bucket index N = 0..4095 for a data set (that is to be added or retrieved), you can locate the offset to the bucket file block directly by reading the 8-byte integer at file offset D = 264 + N*8.

Bucket blocks store the 32-bit hash code, the 64-bit "associated views" field, and an 8-byte file pointer for each data set that is stored in the file and is a member of that bucket. As mentioned above, the hash code is stored so that the repository can verify that a data set to be added to the bucket is not an exact duplicate of an existing set. If the new data set's hash code does not match any of the hash codes currently in the bucket, then it is safe to conclude that it's not a duplicate. If there is a match, then it is necessary to load the existing data set and compare it for equality with the data set being added.

The implementation permits up to 216 data sets per bucket -- indexed from 0 to 65535. However, in practical usage, it is expected that any given bucket will contain far fewer than the maximum. Thus, when the first data set is added to a bucket, the initial bucket file block is allocated to hold the hash code, bit field, and file pointer for up to 32 data sets. When the 33rd data set is added to the bucket, a second bucket block is allocated and appended at file's end, and its file offset is stored in the header of the preceding bucket block -- resulting in a linked-list of bucket blocks. The advantage in this design is that we do not waste file space (if there was a single bucket block per bucket, we'd have to reallocate it and copy the old block to it -- leaving the old block abandoned), at the expense of more file seeks as we traverse the linked list for a particular slot in the bucket.

Each bucket block begins with the 8-byte file offset to the next block in the bucket. This will always be 0 for the last block in the bucket block list. The "next block offset" is followed immediately by 32 20-byte slots. The first 4 bytes of a slot store the data set's hash code, the next 8 hold the "associated views" bit field, and the remaining 8 bytes hold the file offset to the start of the file "data block" in which the actual data set is stored. If a slot is unused, all bytes are set to zero. If the repository file has never been compacted and the data bucket currently contains N sets, then the bucket will have a linked list of min(1, floor(N/32)) bucket blocks, and only the last block will have unused slots, all of which appear at the tail end of the block (no "holes"). Compaction removes data sets from the file, so it can introduce unused slots anywhere within a bucket block list.

Given this file structure, we can summarize the work involved in adding a data set S to the repository:

Retrieving a dataset from the repository is easy. Masking out all but the lower 12 bits of the DUID yields the bucket index B. Read the 8-byte offset at file location 264 + 8*B to get the offset D to the first block in the bucket block list. The data set's slot index P within that bucket is given by (DUID >> 12) & (0xFFFF). We traverse the linked list of bucket blocks to find the P-th bucket slot and read the file offset to the data block. Once we have the data block offset, it's simply a matter of reading in and parsing that block.

Observe that bucket blocks and data set blocks will be interspersed with each other as the repository is populated with data. New blocks -- either bucket blocks or data blocks -- are always appended to the end of the file, so the file's size prior to writing will be the new block's file offset.

Data set block format.

NOTE that the dataset ID string is NOT stored. Data sets in a hub are injected into view templates for display; their string IDs have no significance; it is the set DUID that matters.

File format description: figarch.dnf

The figure archive metadata repository file is structured for random file access much like the hub contents file, beginning with an 8-byte header, followed by a 2KB index block. These two sections are present even if no information is stored in the file. Their format is the same as for hub.dnc, except that the unique file tag constant for figarch.dnf is 0x464E4440 ("@DNF"), and the current version number is 1.

The header serves to identify the file as a DataNav figure archive metadata file. It is structured as follows:

The content and structure of an occupied file block in figarch.dnf varies with the block type: