Skip to main content
overcache

Dive into Metro: Jest Worker

本篇作为 Dive into Metro 系列的第一篇文章, 介绍 Metro 的多任务机制.

总所周知, js 运行机制是单线程的, 同一时刻只能运行一个任务, 因而没法利用多个处理器核心. 但是如今早已是多核的时代, 遑论服务器市场, 家用的 CPU 也是动辄8 核/16 核 起步.

作为一款构建工具, Metro 集合了 IO 密集和计算密集的任务, 在构建 HasteMap 以及转译源码等场景都有多任务并行处理的需求, 合理利用多核资源是它提高效率的不二法宝, 而底层为 Metro 提供多任务机制的工具, 正是本文的主角: 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, 但内部其实是叫 Jest Farm, 为了和普遍意义上的 worker 区分开, 本文余下部分也将之称为 Farm.

Jest Farm 的核心 API 非常简洁, 只需要传递一个 workerPath 即可实例化一个 Farm 对象, 它提供了以下功能:

  • 方法代理, wokerPath 所暴露的方法, 都可以在这个实例上直接调用.
  • 并行处理, 默认会创建物理核心数 - 1 的进程, wokerPath 的方法调用会传递给这些具体的进程执行
  • 任务队列, 支持入先进先出队列及优先级队列
  • 任务调度, 可以支持多种调度机制: sticky/in-roder/round-robin
  • 错误重试/懒加载等

除 wokerPath 一个必填选项外, 也可使用丰富的可选参数定制 Farm 的行为:

export type FarmOptions = {
  computeWorkerKey?: (method: string, ...args: Array<unknown>) => string | null;
  enableWorkerThreads?: boolean;
  exposedMethods?: ReadonlyArray<string>;
  forkOptions?: ForkOptions;
  maxRetries?: number;
  numWorkers?: number;
  resourceLimits?: ResourceLimits;
  setupArgs?: Array<unknown>;
  taskQueue?: TaskQueue;
  WorkerPool?: new (
    workerPath: string,
    options?: WorkerPoolOptions,
  ) => WorkerPoolInterface;
  workerSchedulingPolicy?: WorkerSchedulingPolicy;
};

方法代理

可以通过 exposedMethods 显示声明需要代理 wokerPath 的哪些方法, 也可以不加指定, 由 Farm 自己 require workerPath, 用 Object.keys 代理该文件暴露出来的所有方法.

Jest Farm 实例化时, 会给实例添加相关的属性, 供外界调用.

并行模型

向外暴露出来的 workerPath 的方法, 经过了一层包装, 以提供并行的能力.

Jest Farm 提供了两种并行机制:

  1. ChildProcessWorker, 默认方式
  2. NodeThreadsWorker, 实验性特性, 利用 nodejs 的 threading API 可以显著提高父进程和子进程之间的通信耗时

以默认的 ChildProcessWorker 为例:

  1. 实现一个中间人, 以 js 文件的形式存在, 该中间人充当 Jest Farm 和实际的 workerPath 之间的桥梁.
  2. Jest Farm 根据 numWorkers 创建相应数量的 ChildProcessWorker 实例, 每个 ChildProcessWorker 实例上都用 child_process.fork fork 一个中间人的子进程
  3. 当调用 Jest Farm 暴露出来的方法时, ChildProcessWorker 构造一个包含"调用方法,调用参数"等内容的消息, 传递至中间人
  4. 中间人根据父进程传递的信息, require 实际的 workerPath 文件, 然后调用真正的方法完成任务, 将结果再传递回父进程

任务调度

虽说官方默认实现了 FIFO 以及 priority 队列, 但只要实现了 enqueue/dequeue 方法, 你可以传入自定义的队列来实现特殊的调度功能. 在没有传入 taskQueue 参数时, 默认采用 FIFO 队列.

对任务队列的调度, 有几种方式:

  1. sticky, 根据需要执行的方法及参数, 计算出一个确定的进程执行任务
  2. in-order, 每次都从进程池里从头到尾到一个空闲的进程执行任务
  3. round-robin, 轮询, 从上次挑选的进程后一个进程开始, 向后找一个空闲进程执行任务

新任务抵达时:

  1. 将任务添加到队列中
  2. 根据调度方法, 寻找空闲的进程执行任务. 若没有空闲进程, 便等待
  3. 每个进程执行完任务后, 会从队列中拿一个任务进行执行
  4. 如此循环

总结

以上, 就是 jest-worker 的简单介绍, 这个模块在 facebook 内部的其他项目中也有大量的使用, 如 Jest. 在 metro 中, 主要在以下场景下使用:

  • 构建 HasteMap
  • Transform 源文件