tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

data_cache.ts (6370B)


      1 /**
      2 * Utilities to improve the performance of the CTS, by caching data that is
      3 * expensive to build using a two-level cache (in-memory, pre-computed file).
      4 */
      5 
      6 import { assert } from '../util/util.js';
      7 
      8 interface DataStore {
      9  load(path: string): Promise<Uint8Array>;
     10 }
     11 
     12 /** Logger is a basic debug logger function */
     13 export type Logger = (s: string) => void;
     14 
     15 /**
     16 * DataCacheNode represents a single cache entry in the LRU DataCache.
     17 * DataCacheNode is a doubly linked list, so that least-recently-used entries can be removed, and
     18 * cache hits can move the node to the front of the list.
     19 */
     20 class DataCacheNode {
     21  public constructor(path: string, data: unknown) {
     22    this.path = path;
     23    this.data = data;
     24  }
     25 
     26  /** insertAfter() re-inserts this node in the doubly-linked list after `prev` */
     27  public insertAfter(prev: DataCacheNode) {
     28    this.unlink();
     29    this.next = prev.next;
     30    this.prev = prev;
     31    prev.next = this;
     32    if (this.next) {
     33      this.next.prev = this;
     34    }
     35  }
     36 
     37  /** unlink() removes this node from the doubly-linked list */
     38  public unlink() {
     39    const prev = this.prev;
     40    const next = this.next;
     41    if (prev) {
     42      prev.next = next;
     43    }
     44    if (next) {
     45      next.prev = prev;
     46    }
     47    this.prev = null;
     48    this.next = null;
     49  }
     50 
     51  public readonly path: string; // The file path this node represents
     52  public readonly data: unknown; // The deserialized data for this node
     53  public prev: DataCacheNode | null = null; // The previous node in the doubly-linked list
     54  public next: DataCacheNode | null = null; // The next node in the doubly-linked list
     55 }
     56 
     57 /** DataCache is an interface to a LRU-cached data store used to hold data cached by path */
     58 export class DataCache {
     59  public constructor() {
     60    this.lruHeadNode.next = this.lruTailNode;
     61    this.lruTailNode.prev = this.lruHeadNode;
     62  }
     63 
     64  /** setDataStore() sets the backing data store used by the data cache */
     65  public setStore(dataStore: DataStore) {
     66    this.dataStore = dataStore;
     67  }
     68 
     69  /** setDebugLogger() sets the verbose logger */
     70  public setDebugLogger(logger: Logger) {
     71    this.debugLogger = logger;
     72  }
     73 
     74  /**
     75   * fetch() retrieves cacheable data from the data cache, first checking the
     76   * in-memory cache, then the data store (if specified), then resorting to
     77   * building the data and storing it in the cache.
     78   */
     79  public async fetch<Data>(cacheable: Cacheable<Data>): Promise<Data> {
     80    {
     81      // First check the in-memory cache
     82      const node = this.cache.get(cacheable.path);
     83      if (node !== undefined) {
     84        this.log('in-memory cache hit');
     85        node.insertAfter(this.lruHeadNode);
     86        return Promise.resolve(node.data as Data);
     87      }
     88    }
     89    this.log('in-memory cache miss');
     90    // In in-memory cache miss.
     91    // Next, try the data store.
     92    if (this.dataStore !== null && !this.unavailableFiles.has(cacheable.path)) {
     93      let serialized: Uint8Array | undefined;
     94      try {
     95        serialized = await this.dataStore.load(cacheable.path);
     96        this.log('loaded serialized');
     97      } catch (err) {
     98        // not found in data store
     99        this.log(`failed to load (${cacheable.path}): ${err}`);
    100        this.unavailableFiles.add(cacheable.path);
    101      }
    102      if (serialized !== undefined) {
    103        this.log(`deserializing`);
    104        const data = cacheable.deserialize(serialized);
    105        this.addToCache(cacheable.path, data);
    106        return data;
    107      }
    108    }
    109    // Not found anywhere. Build the data, and cache for future lookup.
    110    this.log(`cache: building (${cacheable.path})`);
    111    const data = await cacheable.build();
    112    this.addToCache(cacheable.path, data);
    113    return data;
    114  }
    115 
    116  /**
    117   * addToCache() creates a new node for `path` and `data`, inserting the new node at the front of
    118   * the doubly-linked list. If the number of entries in the cache exceeds this.maxCount, then the
    119   * least recently used entry is evicted
    120   * @param path the file path for the data
    121   * @param data the deserialized data
    122   */
    123  private addToCache(path: string, data: unknown) {
    124    if (this.cache.size >= this.maxCount) {
    125      const toEvict = this.lruTailNode.prev;
    126      assert(toEvict !== null);
    127      toEvict.unlink();
    128      this.cache.delete(toEvict.path);
    129      this.log(`evicting ${toEvict.path}`);
    130    }
    131    const node = new DataCacheNode(path, data);
    132    node.insertAfter(this.lruHeadNode);
    133    this.cache.set(path, node);
    134    this.log(`added ${path}. new count: ${this.cache.size}`);
    135  }
    136 
    137  private log(msg: string) {
    138    if (this.debugLogger !== null) {
    139      this.debugLogger(`DataCache: ${msg}`);
    140    }
    141  }
    142 
    143  // Max number of entries in the cache before LRU entries are evicted.
    144  private readonly maxCount = 4;
    145 
    146  private cache = new Map<string, DataCacheNode>();
    147  private lruHeadNode = new DataCacheNode('', null); // placeholder node (no path or data)
    148  private lruTailNode = new DataCacheNode('', null); // placeholder node (no path or data)
    149  private unavailableFiles = new Set<string>();
    150  private dataStore: DataStore | null = null;
    151  private debugLogger: Logger | null = null;
    152 }
    153 
    154 /** The data cache */
    155 export const dataCache = new DataCache();
    156 
    157 /** true if the current process is building the cache */
    158 let isBuildingDataCache = false;
    159 
    160 /** @returns true if the data cache is currently being built */
    161 export function getIsBuildingDataCache() {
    162  return isBuildingDataCache;
    163 }
    164 
    165 /** Sets whether the data cache is currently being built */
    166 export function setIsBuildingDataCache(value = true) {
    167  isBuildingDataCache = value;
    168 }
    169 
    170 /**
    171 * Cacheable is the interface to something that can be stored into the
    172 * DataCache.
    173 * The 'npm run gen_cache' tool will look for module-scope variables of this
    174 * interface, with the name `d`.
    175 */
    176 export interface Cacheable<Data> {
    177  /** the globally unique path for the cacheable data */
    178  readonly path: string;
    179 
    180  /**
    181   * build() builds the cacheable data.
    182   * This is assumed to be an expensive operation and will only happen if the
    183   * cache does not already contain the built data.
    184   */
    185  build(): Promise<Data>;
    186 
    187  /**
    188   * serialize() encodes `data` to a binary representation so that it can be stored in a cache file.
    189   */
    190  serialize(data: Data): Uint8Array;
    191 
    192  /**
    193   * deserialize() is the inverse of serialize(), decoding the binary representation back to a Data
    194   * object.
    195   */
    196  deserialize(binary: Uint8Array): Data;
    197 }