Silk 概述

../_images/SilkArchitecture.png

架构

我们当前的架构是将三个组件与硬件垂直同步定时器对齐

  1. 合成器

  2. 刷新驱动程序 / 绘制

  3. 输入事件

渲染引擎的流程如下

  1. 硬件垂直同步事件在每个显示器上基于操作系统的 硬件垂直同步线程 上发生。

  2. 连接到显示器的 硬件垂直同步线程 通知 CompositorVsyncDispatchersVsyncDispatcher

  3. 对于特定显示器上的每个 Firefox 窗口,都会通知一个 CompositorVsyncDispatcherCompositorVsyncDispatcher 是特定于一个窗口的。

  4. 当进行远程合成时, CompositorVsyncDispatcher 会在远程合成时通知 CompositorWidgetVsyncObserver,或者在进程内合成时通知 CompositorVsyncScheduler::Observer

  5. 如果进行远程合成,则垂直同步通知会从 CompositorWidgetVsyncObserver 发送到 UI 进程上的 VsyncBridgeChild,后者会向 GPU 进程的合成器线程上的 VsyncBridgeParent 发送 IPDL 消息,然后分派到 CompositorVsyncScheduler::Observer

  6. VsyncDispatcher 通知 Chrome 的 RefreshTimer 发生了垂直同步。

  7. VsyncDispatcher 向所有内容进程发送 IPC 消息,以使它们各自的活动 RefreshTimer 计时。

  8. Compositor合成器线程 上分派输入事件,然后进行合成。仅在 b2g 上的 合成器线程 上分派输入事件。

  9. RefreshDriver主线程 上绘制。

硬件垂直同步

来自 (1) 的硬件垂直同步事件,发生在特定的 Display 对象上。 Display 对象负责在每个连接的显示器上启用/禁用垂直同步。例如,如果连接了两个显示器,则会创建两个 Display 对象,每个对象分别侦听其显示器的垂直同步事件。我们每个显示器需要一个 Display 对象,因为每个显示器的垂直同步速率可能不同。作为备用解决方案,我们有一个全局 Display 对象,可以在所有连接的显示器之间同步。全局 Display 在窗口位于两个显示器之间时很有用。每个平台都必须实现一个特定的 Display 对象来挂接和侦听垂直同步事件。在撰写本文时,Firefox OS 和 OS X 都创建了自己的特定于硬件的 硬件垂直同步线程,该线程在垂直同步发生后执行。OS X 为每个 CVDisplayLinkRef 创建一个 硬件垂直同步线程。我们目前不支持多个显示器,因此我们使用一个适用于所有活动显示器的全局 CVDisplayLinkRef。在 Windows 上,我们必须创建一个新的平台 thread 来等待 DwmFlush(),它适用于所有活动显示器。线程从 DwmFlush() 唤醒后,实际的垂直同步时间戳将从 DwmGetCompositionTimingInfo() 中检索,这是实际传递给合成器和刷新驱动程序的时间戳。

当显示器上发生垂直同步时,硬件垂直同步线程 回调函数获取与 Display 关联的所有 CompositorVsyncDispatchers。每个 CompositorVsyncDispatcher 会收到通知,告知已发生垂直同步以及垂直同步的时间戳。 CompositorVsyncDispatcher 负责通知正在等待垂直同步通知的 Compositor。然后, Display 会通知关联的 VsyncDispatcher,后者应通知所有活动的 RefreshDrivers 进行计时。

所有 Display 对象都封装在一个 VsyncSource 对象中。 VsyncSource 对象位于 gfxPlatform 中,并且仅在创建 gfxPlatform 时在父进程上实例化。 VsyncSource 在销毁 gfxPlatform 时被销毁。当布局帧率首选项(或影响帧率的其他首选项)更改时,它也可以被销毁。这可能意味着我们在运行时从硬件垂直同步切换到软件垂直同步(反之亦然)。在切换期间,可能会有短暂的 2 个垂直同步源。否则,在 Firefox 的整个生命周期中,只有一个 VsyncSource 对象。每个平台都应该实现自己的 VsyncSource 来管理垂直同步事件。在 OS X 上,这是通过 CVDisplayLinkRef 实现的。在 Windows 上,它应该通过 DwmGetCompositionTimingInfo 实现。

合成器

CompositorVsyncDispatcher 收到垂直同步事件通知时,与 CompositorVsyncDispatcher 关联的 CompositorVsyncScheduler::Observer 开始执行。由于 CompositorVsyncDispatcher硬件垂直同步线程 上执行,而 CompositorCompositorThread 上进行合成,因此 CompositorVsyncScheduler::Observer 将任务发布到 CompositorThread。然后, CompositorBridgeParent 进行合成。 CompositorVsyncDispatcher硬件垂直同步线程 上通知组件,并且组件在适当的线程上调度任务的模型在所有地方都使用。

CompositorVsyncScheduler::Observer 根据需要侦听垂直同步事件,并在不再调度或需要合成时停止侦听垂直同步。每个 CompositorBridgeParent 都与一个 CompositorVsyncScheduler::Observer 关联并绑定,后者与 CompositorVsyncDispatcher 关联。每个 CompositorBridgeParent 与一个窗口小部件关联,并在创建新的平台窗口或 nsBaseWidget 时创建。 CompositorBridgeParentCompositorVsyncDispatcherCompositorVsyncScheduler::ObservernsBaseWidget 具有相同的使用寿命,它们一起创建和销毁。

进程外合成器

当进行进程外合成时,此模型会略有变化。在这种情况下,实际上有两个观察者:一个 UI 进程观察者 (CompositorWidgetVsyncObserver) 和 GPU 进程中的 CompositorVsyncScheduler::Observer。此外,还有两个分派器:UI 进程中的窗口小部件分派器 (CompositorVsyncDispatcher) 和 GPU 进程中基于 IPDL 的分派器 (CompositorBridgeParent::NotifyVsync)。UI 进程观察者和 GPU 进程分派器通过称为 PVsyncBridge 的 IPDL 协议链接。 PVsyncBridge 是一个顶层协议,用于将垂直同步通知发送到 GPU 进程中的合成器线程。合成器通过一个单独的 actor (PCompositorWidget) 控制垂直同步观察,该 actor(作为 CompositorBridgeChild 的子 actor)将 GPU 进程中的合成器线程链接到 UI 进程中的主线程。

进程外合成器不会直接通过 CompositorVsyncDispatcher。相反,UI 进程中的 CompositorWidgetDelegate 会创建一个,并为其提供一个 CompositorWidgetVsyncObserver。此观察者将通知转发到垂直同步 I/O 线程,然后 VsyncBridgeChild 将通知再次转发到 GPU 进程中的合成器线程。通知由 VsyncBridgeParent 接收。GPU 进程使用通知中的图层 ID 来查找要将通知分派到的正确合成器。

CompositorVsyncDispatcher

CompositorVsyncDispatcher 在 **硬件垂直同步线程 (Hardware Vsync Thread)** 上执行。它包含与其关联的 nsBaseWidget 的引用,并且生命周期与 nsBaseWidget 相同。 CompositorVsyncDispatcher 负责通知 CompositorBridgeParent 发生了垂直同步事件。每个 Display 可以有多个 CompositorVsyncDispatchers,每个窗口一个 CompositorVsyncDispatcherCompositorVsyncDispatcher 的唯一职责是在发生垂直同步事件时通知组件,并在没有组件需要垂直同步事件时停止监听垂直同步。我们需要每个窗口一个 CompositorVsyncDispatcher,以便我们可以处理多个 Displays。在进程内合成时, CompositorVsyncDispatcher 附加到窗口的 CompositorWidget 上。在进程外时,它附加到 CompositorWidgetDelegate 上,后者通过 IPDL 转发观察者通知。在后一种情况下,其生命周期与 CompositorSession 而不是 nsIWidget 相关联。

多个显示器

VsyncSource 有一个 API 可以将 CompositorVsyncDispatcher 从一个 Display 切换到另一个 Display。例如,当一个窗口进入全屏模式或从一个连接的显示器移动到另一个显示器时。当一个窗口移动到另一个显示器时,我们期望发生平台特定的通知。窗口何时进入全屏模式或移动的检测不受 Silk 本身覆盖,但框架构建为支持此用例。预期的流程是 OS 通知在 nsIWidget 上发生,它检索关联的 CompositorVsyncDispatcher。然后, CompositorVsyncDispatcher 通知 VsyncSource 切换到 CompositorVsyncDispatcher 连接到的正确的 Display。由于通知通过 nsIWidget 工作,因此将 CompositorVsyncDispatcher 切换到正确的 Display 的实际操作应该在 **主线程 (Main Thread)** 上发生。Silk 的当前实现不处理这种情况,需要构建出来。

CompositorVsyncScheduler::Observer

CompositorVsyncScheduler::Observer 处理垂直同步通知以及与 CompositorVsyncDispatcher 的交互。当 Compositor 需要计划的合成时,它会通知 CompositorVsyncScheduler::Observer 需要监听垂直同步。然后, CompositorVsyncScheduler::Observer 根据需要从 CompositorVsyncDispatcher 观察/取消观察垂直同步以启用合成。

GeckoTouchDispatcher

GeckoTouchDispatcher 是一个单例,它重新采样触摸事件以在跟踪用户手指的同时消除卡顿。由于输入和合成是关联在一起的,因此 CompositorVsyncScheduler::Observer 具有对 GeckoTouchDispatcher 的引用,反之亦然。

输入事件

Silk 的一个主要目标是将触摸事件与垂直同步事件对齐。在 Firefox OS 上,触摸屏的触摸扫描速率通常与显示器刷新速率不同。Flame 设备的触摸刷新率为 75 HZ,而 Nexus 4 的触摸刷新率为 100 HZ,而设备的显示器刷新率为 60HZ。当发生垂直同步事件时,我们重新采样触摸事件,然后将重新采样的触摸事件分派到 APZ。Firefox OS 上的触摸事件发生在 **触摸输入线程 (Touch Input Thread)** 上,而它们由 APZ 在 **APZ 控制器线程 (APZ Controller Thread)** 上处理。我们使用 Google Android 的触摸重新采样 算法来重新采样触摸事件。

目前,我们在合成和触摸事件之间有一个严格的顺序。当触摸事件在 **触摸输入线程 (Touch Input Thread)** 上发生时,我们将触摸事件存储在队列中。当发生垂直同步事件时, CompositorVsyncDispatcher 通知 Compositor 发生了垂直同步事件,后者通知 GeckoTouchDispatcherGeckoTouchDispatcher 首先在 **APZ 控制器线程 (APZ Controller Thread)** 上处理触摸事件,该线程与 b2g 上的 **合成线程 (Compositor Thread)** 相同,然后 Compositor 完成合成。我们要求这种严格的顺序是因为,如果同时向 CompositorGeckoTouchDispatcher 分派垂直同步通知,则在处理触摸事件(因此是位置)与合成之间会出现竞争条件。在实践中,这会导致非常卡顿的滚动。截至撰写本文时,我们尚未分析桌面平台上的输入事件。

一个轻微的怪癖是输入事件可以启动合成,例如在滚动期间以及 Compositor 不再监听垂直同步事件之后。在这些情况下,我们通知 Compositor 观察垂直同步,以便它分派触摸事件。如果触摸事件未分派,并且由于 Compositor 未监听垂直同步事件,则触摸事件将永远不会分派。 GeckoTouchDispatcher 通过始终强制 Compositor 在发生触摸事件时监听垂直同步事件来处理这种情况。

Widget、Compositor、CompositorVsyncDispatcher、GeckoTouchDispatcher 关闭过程

nsBaseWidget 关闭 时 - 它在 **Gecko 主线程 (Gecko Main Thread)** 上调用 nsBaseWidget::DestroyCompositor。在 nsBaseWidget::DestroyCompositor 期间,它首先销毁 CompositorBridgeChild。CompositorBridgeChild 发送一个同步 IPC 调用到 CompositorBridgeParent::RecvStop,后者调用 CompositorBridgeParent::Destroy。在此期间,**主线程 (main thread)** 在父进程中被阻塞。CompositorBridgeParent::RecvStop 在 **合成线程 (Compositor thread)** 上运行并清理一些资源,包括将 CompositorVsyncScheduler::Observer 设置为 nullptr。CompositorBridgeParent::RecvStop 还明确地使 CompositorBridgeParent 保持活动状态,并发布另一个任务以在合成循环上运行 CompositorBridgeParent::DeferredDestroy,以便所有 ipdl 代码可以完成执行。 CompositorVsyncScheduler::Observer 还会取消观察垂直同步并取消任何挂起的合成任务。CompositorBridgeParent::RecvStop 完成后,父进程中的 **主线程 (main thread)** 继续关闭 nsBaseWidget。

同时,**合成线程 (Compositor thread)** 正在执行任务,直到 CompositorBridgeParent::DeferredDestroy 运行,后者刷新合成消息循环。现在我们有两个任务,因为 nsBaseWidget 在销毁期间在 **主线程 (main thread)** 上释放对 Compositor 的引用,而 CompositorBridgeParent::DeferredDestroy 在 **合成线程 (Compositor Thread)** 上释放对 CompositorBridgeParent 的引用。最后,CompositorBridgeParent 本身在两个引用都消失后在 **主线程 (main thread)** 上被销毁,这是由于显式 主线程销毁 导致的。

对于 CompositorVsyncScheduler::Observer,在 nsBaseWidget::DestroyCompositor 执行后对 widget 的任何访问都是无效的。在 nsBaseWidget::DestroyCompositor 运行和 CompositorVsyncScheduler::Observer 的析构函数运行之间对合成器的任何访问都不安全,因为在此期间可能会发生硬件垂直同步事件。由于在发布 CompositorBridgeParent::DeferredDestroy 后在合成循环上发布的任何任务都是无效的,因此我们确保一旦 CompositorBridgeParent::RecvStop 执行并发布 DeferredDestroy 在合成线程上,就不会发布任何垂直同步任务。当对 CompositorBridgeParent::RecvStop 的同步调用执行时,我们显式地将 CompositorVsyncScheduler::Observer 设置为 null,以防止发生垂直同步通知。如果允许垂直同步通知发生,由于 CompositorVsyncScheduler::Observer 的垂直同步通知在 **硬件垂直同步线程 (hardware vsync thread)** 上执行,它将向合成循环发布一个任务,并且可能在 CompositorBridgeParent::DeferredDestroy 之后执行。因此,我们在 nsBaseWidget::Shutdown 期间显式关闭 CompositorVsyncDispatcherCompositorVsyncScheduler::Observer 中的垂直同步事件,以防止任何垂直同步任务在 CompositorBridgeParent::DeferredDestroy 之后执行。

CompositorVsyncDispatcher 可以在 **主线程 (main thread)** 或 **合成线程 (Compositor Thread)** 上销毁,因为 nsBaseWidget 和 CompositorVsyncScheduler::Observer 都在不同的线程上争夺销毁。nsBaseWidget 在 **主线程 (main thread)** 上销毁,并在销毁期间释放对 CompositorVsyncDispatcher 的引用。 CompositorVsyncScheduler::Observer 存在一个竞争条件,它可能在 CompositorBridgeParent 关闭期间或在 GeckoTouchDispatcher 中销毁,后者使用 ClearOnShutdown 在主线程上销毁。无论哪个对象,CompositorBridgeParent 还是 GeckoTouchDispatcher 最后被销毁,都将持有对 CompositorVsyncDispatcher 的最后一个引用,从而销毁该对象。

刷新驱动程序

刷新驱动程序从 单个活动计时器 中触发。假设有多个 RefreshDrivers 连接到单个 RefreshTimer。有两个 RefreshTimers:一个活动计时器和一个非活动计时器。每个选项卡都有自己的 RefreshDriver,它连接到其中一个全局 RefreshTimersRefreshTimers 在 **主线程 (Main Thread)** 上执行并触发其连接的 RefreshDrivers。我们不想破坏这种每个选项卡都有自己的刷新驱动程序的模型。每个 RefreshDriver 在活动和非活动 RefreshTimer 之间切换。

相反,我们创建一个新的 RefreshTimer,即 VsyncRefreshTimer,它基于垂直同步消息触发。我们将当前活动计时器替换为 VsyncRefreshTimer。然后,所有选项卡都将基于此新的活动计时器触发。由于 RefreshTimer 的生命周期是进程,因此我们只需要在 Firefox 启动时为每个 Display 创建一个 VsyncDispatcher。即使我们没有任何内容进程,Chrome 进程仍然需要一个 VsyncRefreshTimer,因此我们可以将 VsyncDispatcher 与每个 Display 关联。

当 Firefox 启动时,我们最初会在 Chrome 进程中创建一个新的 VsyncRefreshTimerVsyncRefreshTimer 将监听来自全局 Display 上的 VsyncDispatcher 的垂直同步通知。当 nsRefreshDriver::Shutdown 执行时,它将删除 VsyncRefreshTimer。这会产生一个问题,因为所有 RefreshTimers 目前都是手动内存管理的,而 VsyncObservers 则是引用计数的。为了解决这个问题,我们创建了一个新的 RefreshDriverVsyncObserver 作为 VsyncRefreshTimer 的内部类,它实际上接收垂直同步通知。然后它在 VsyncRefreshTimer 内触发 RefreshDrivers

对于内容进程,启动过程更加复杂。我们通过使用父进程上的 PBackground 线程发送垂直同步 IPC 消息,这使我们能够在不等待主线程的情况下从父进程发送消息。这会将消息从 Parent::PBackground 线程发送到 Child::主线程。内容进程上接收 IPC 消息的主线程是可以接受的,因为 RefreshDrivers 必须在主线程上执行。但是,在进程创建时和这段时间内,建立 IPC 连接需要一些时间,RefreshDrivers 必须触发以设置进程。为了解决这个问题,我们最初使用在内容进程启动期间已经存在的软件 RefreshTimers,并在创建 IPC 连接后切换到 VsyncRefreshTimer

在 nsRefreshDriver::ChooseTimer 期间,我们创建一个异步 PBackground IPC 打开请求以创建 VsyncParentVsyncChild。同时,我们创建一个软件 RefreshTimer 并像往常一样触发 RefreshDrivers。一旦 PBackground 回调执行并且存在 IPC 连接,我们就交换当前与活动 RefreshTimer 关联的所有 RefreshDrivers,并将 RefreshDrivers 切换为使用 VsyncRefreshTimer。由于内容进程上的所有交互都发生在主线程上,因此不需要锁。 VsyncParent 通过父端上的 VsyncRefreshTimerDispatcher 监听垂直同步事件,并将垂直同步 IPC 消息发送到 VsyncChildVsyncChild 通知内容进程上的 VsyncRefreshTimer

在内容进程的关闭过程中,由于正常的 PBackground 关闭过程,会在 VsyncChildVsyncParent 上调用 ActorDestroy。一旦调用 ActorDestroy,就不应再通过通道发送 IPC 消息。调用 ActorDestroy 后,IPDL 机制将删除 VsyncParent/Child 对。由于 VsyncParent 是一个 VsyncObserver,因此它是引用计数的。在调用 VsyncParent::ActorDestroy 后,它会从 VsyncDispatcher 中注销自身,后者持有对 VsyncParent 的最后一个引用,并且该对象将被删除。

因此,在正常执行期间的整体流程是

  1. VsyncSource::Display::VsyncDispatcher 从父进程中的操作系统接收垂直同步通知。

  2. VsyncDispatcher 通知 VsyncRefreshTimer::RefreshDriverVsyncObserver 在父进程的硬件垂直同步线程上发生了垂直同步。

  3. VsyncDispatcher 通知硬件垂直同步线程上的 VsyncParent 发生了垂直同步。

  4. 父进程中的 VsyncRefreshTimer::RefreshDriverVsyncObserver 将一个任务发布到主线程,以触发刷新驱动程序。

  5. VsyncParent 将一个任务发布到 PBackground 线程,以发送垂直同步 IPC 消息到 VsyncChild。

  6. VsyncChild 在内容进程的主线程上收到垂直同步通知,并触发其各自的 RefreshDrivers。

压缩垂直同步消息

垂直同步消息出现的频率非常高,并且由于 JavaScript,主线程可能会长时间处于繁忙状态。持续向刷新驱动程序计时器发送垂直同步消息可能会使主线程充斥着刷新驱动程序触发,从而导致更多延迟。为了避免此问题,我们在父进程和子进程上都压缩垂直同步消息。

在父进程中,较新的垂直同步消息会更新垂直同步时间戳,但实际上不会在主线程上排队任何任务。一旦父进程的主线程执行刷新驱动程序触发,它就会使用最新的垂直同步时间戳来触发刷新驱动程序。刷新驱动程序触发后,会为另一个刷新驱动程序触发任务排队一个垂直同步消息。在内容进程中,IPDL compress 关键字会自动压缩 IPC 消息。

多显示器

为了支持 RefreshDrivers 的多显示器功能,我们有多个活动的 RefreshTimers。每个 RefreshTimer 通过 ID 与特定的 Display 关联,并在其各自的 Display 发生垂直同步时触发。我们有 N 个 RefreshTimers,其中 N 是连接的显示器的数量。每个 RefreshTimer 仍然有多个 RefreshDrivers

当选项卡或窗口更改显示器时, nsIWidget 会收到显示器更改通知。根据窗口所在的显示器,窗口会切换到父进程中基于显示器 ID 的正确的 VsyncDispatcherCompositorVsyncDispatcher。每个 TabParent 还应向其子级发送通知。每个 TabChild 根据显示器 ID 切换到与显示器 ID 关联的正确的 RefreshTimer。当每个显示器发生垂直同步时,它会发送一条 IPC 消息来通知垂直同步。垂直同步消息包含一个显示器 ID,以便在内容进程上触发相应的 RefreshTimer。仍然只有一个 VsyncParent/VsyncChild 对,只是每个垂直同步通知都将包含一个显示器 ID,该 ID 映射到正确的 RefreshTimer

对象生命周期

  1. CompositorVsyncDispatcher - 与 VsyncDispatcher 关联的 nsBaseWidget 的生命周期一样长。

  2. CompositorVsyncScheduler::Observer - 与 CompositorBridgeParent 的生命周期相同。

  3. VsyncDispatcher - 与关联的显示对象一样长,即 Firefox 的生命周期。

  4. VsyncSource - 与 Chrome 进程上的 gfxPlatform 一样长,即 Firefox 的生命周期。

  5. VsyncParent/VsyncChild - 与内容进程一样长。

  6. RefreshTimer - 与进程一样长。

线程

所有 VsyncObservers 都会在硬件垂直同步线程上收到通知。 VsyncObservers 负责将其任务发布到各自的正确线程。例如, CompositorVsyncScheduler::Observer 将在硬件垂直同步线程上收到通知,并将任务发布到合成线程以进行实际的合成操作。

  1. 合成线程 - 没有变化。

  2. 主线程 - PVsyncChild 在主线程上接收 IPC 消息。我们还在主线程上启用/禁用垂直同步。

  3. PBackground 线程 - 在父进程的 PBackground 线程上创建到内容进程主线程的连接。

  4. 硬件垂直同步线程 - 每个平台都不同,但我们始终具有硬件垂直同步线程的概念。有时这实际上是由主机操作系统创建的。在 Windows 上,我们必须创建一个单独的平台线程,该线程在 DwmFlush() 上阻塞。