Skip to main content
overcache

Dive into Metro: HasteMap

HasteMap is a JavaScript implementation of Facebook's haste module system. This implementation is inspired by https://github.com/facebook/node-haste and was built with for high-performance in large code repositories with hundreds of thousands of files. This implementation is scalable and provides predictable performance.
Because the haste map creation and synchronization is critical to startup performance and most tasks are blocked by I/O this class makes heavy use of synchronous operations. It uses worker processes for parallelizing file access and metadata extraction.

作为一个打包器, 和文件系统打交道是不可避免的, 获取文件内容是最基础的要求, 如果要做热更新, 那就还需要做文件系统的监听, 使得文件在发生改变时打包器能知晓并作出对应的动作. Metro 使用了 HasteMap 来对文件系统进行抽象, 该类由 jest-haste-map 包提供, 本文聊聊 HasteMap.

数据结构

HasteMap 内部采用了多个数据结构来对文件系统进行抽象, 以下是重要的几种数据类型:

  • FileMetaData, 表示单个文件的数据, 包含文件大小, 哈希值, 修改时间等
  • ModuleMapData, 表示单个模块的数据, 模块信息包含 id 以及不同平台下, 该模块的文件路径
  • HasteFS, 封装了文件数据的集合以及对获取文件元信息的操作.
  • ModuleMap, 对模块集合的封装

FileMetaData

虽说是对文件系统的抽象, 其实也只提取了部分感兴趣的文件元数据, 而且基于性能的考虑, 文件的元数据采用了数组作为其数据结构:

export type FileMetaData = [
  /* id */ string,
  /* mtime */ number,
  /* size */ number,
  /* visited */ 0 | 1,
  /* dependencies */ string,
  /* sha1 */ string | null | undefined,
];

文件的路径, 跟它的元数据一起, 组成了 files 集合:

export type FileData = Map</* relativeFilePath */string, FileMetaData>;

ModuleMapData

在 HasteMap 中, 模块的类型主要有以下两种:

  • PACKAGE(在内部以 0 表示): 文件路径以 pakcage.json 结尾, 当读取到该文件名时, 如果 name 属性有值, 那么它就是一个模块, name 就是模块的 id. metro 中的模块全是这一种
  • MODULE(在内部以 1 表示): 如果实例化 HasteMap 时传入了 hasteImplModulePath, 那么将 require hasteImplModulePath 并调用它的 hasteImpl.getHasteName(filePath) 方法来获取id, 如果 id 非空, 那么该文件也被认为是一个模块

模块数据, 主要存储了以下内容:

export type ModuleMapData = Map</* moduleId */string,
  {
    [platform: string]: [/* relativeFilePath */string, /* type */ number]
  }>;

HasteFS

封装了一些读操作, 方便用于读取 FileMetaData 中的数据

class HasteFS {
  private readonly _files: FileData;

  getSize: (file: string) => number | null
  getDependencies: (file: string) => Array<string> | null
  getSha1: (file: string) => string | null
  exists: (file: string) => boolean
  getAllFiles: () => Array<string>
  ...
}

ModuleMap

对模块集合的封装, metro中用得少, 略过

文件元数据爬取

根据执行环境以及参数选项, 有多种爬取文件系统的方式:

  • 使用 watchman (当watchman可用且useWatchman参数为true时使用)
  • 使用 node API
    • 使用 find 命令获取文件列表(当 find 命令可用时使用)
    • 使用 node API 递归获取文件列表
  1. 通过以上过程, 收集目录中的文件列表, 然后用 fs.stat 获取文件的文件大小和修改时间, 封装在 FileMetaData 内
  2. 接着迭代所有的 FileMetaData, 计算所有文件的哈希值和依赖, 丰满 FileMetaData 的内容. 如果 FileMetaData 对应着一个模块, 那么还将模块放到一个 map 中

Watch mode

除了抓取文件系统的信息, HasteMap 还支持监听文件系统的变化, 当文件发生改变时 emit 出相关的事件通知调用方, 监听变化者称之为 watcher. 和 clawer 一样, HasteMap 中的 watcher 也有多种:

  • WatchmanWatcher, watchman 可用时采用
  • FSEventsWatcher, fsevents 模块可用时采用
  • NodeWatcher, 兜底方案

性能是依次降低, 兜底的 NodeWatcher, 其工作机制大略如下:

  1. walker 遍历文件夹
  2. 对遇到的文件夹用 fs.watch 进行监听
  3. 将文件变化统一为规范的事件后再 emit

缓存系统

以上的过程当然不需要每次都从头开始:

  1. 每次 hasteMap 建立完毕, 会通过 v8.serialize 将内存数据序列化到磁盘中.
  2. 每次建立 hasteMap 前, 从指定的缓存目录中读取文件, 用 v8.deserialize 来反序列化回内存数据

多任务并行

缓存是提高效率的方法之一, 除此之外, HasteMap 还支持利用多个子进程来加速建立数据的过程. 内置的多任务功能由 jest-worker 提供:

Module for executing heavy tasks under forked processes in parallel, by providing a Promise based interface, minimum overhead, and bound workers.
The module works by providing an absolute path of the module to be loaded in all forked processes. All methods are exposed on the parent process as promises, so they can be await'ed. Child (worker) methods can either be synchronous or asynchronous.

实例化 jest-worker 时传入 js 文件地址, 在生成的 Worker 实例上可以直接调用该 js 文件的 export 出来的方法, 并且 Worker 内部封装了任务的调度, 将对 Worker 方法的调用传递给内部的多个子进程中.

更多关于 jest-worker 的内容, 可以参详另一篇博文 Dive into metro: Jest-Worker