合成器线程架构

原文链接:

http://www.chromium.org/developers/design-documents/compositor-thread-architecture

目标

render主线程是一个相当可怕的地方。HTML,CSS,JavaScript,以及Web平台上的一切都运行或起始于render主线程。It routinely stalls for tens to hundreds of milliseconds.在ARM平台,stalls可能是几秒长。可惜的是,阻止所有这些stalls是不可能的:样式重新计算,同步网络请求,绘制时间,垃圾回收,所有这些事情都是内容相关的开销。

合成线程架构允许我们snapshot网页的一个版本,允许用户滚动并直接在snapshot上看到动画,呈现网页平滑运行的假象。

背景

基本的frontend合成架构和chrome gpu 架构的背景知识可以在这里找到:

http://dev.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome

基本实现

合成器被设计成两半:主线程一半,”impl thread”一半。

主线程的一半是典型的layer tree。一层layer包含转换,大小和内容。layers按需求填充:layers可以被破坏(setNeedsDisplayInRect)合成器决定何时运行layerdelegate告诉layer开始绘制。这类似于大多数操作系统中看到的InvalidateRect/Paint模式,只是这里换成了Layer。Layers可以有孩子,可以clip/reflect等,allowing all sorts of neat visual effects to be created.

合成器的impl部分对layer tree的使用者是隐藏的。impl部分几乎完全是主线程树结构的一个克隆---当我们在主线程有一个layer时,这个layer会在impl线程有一个对应的layer。我们的命名可能有点奇怪,但是:

●LayerChromium: layer的主线程版本;

●CCLayerImpl:layer的impl线程版本;

主线程的树结构是webkit想要绘制什么的模型。主线程paintlayer内容textures。 textures被交给impl tree。 impl tree是实际被draw到屏幕上的。我们可以在任何时间draw impl tree,即使是主线程被阻塞的时候。

LayerChromium tree的使用者可以标明layer是可滚动的。通过将输入事件传给主线程之前先传给impl 线程,我们可以在不与主线程协商的情况下滚动和重绘树结构。这允许我们即使在主线程被阻塞的情况下,实现“平滑滚动”。

LayerChromium tree的使用者可以添加动画到树结构中。我们可以在impl tree运行这些动画,允许hitch-free动画。

树同步化,Host和Commits

chromium中的每一个tab都有一个不同的layer tree.每个tab有一个layertree host,管理每个tab的树状态。Again:

●CCLayerTreeHost:持有LayerChromiumtree;

  CCLayerTreeHost:m_rootLayer;

●CCLayerTreeHostImpl:持有CCLayerImpltree;

  CCLayerTreeHostImpl:m_rootLayer;

这两颗树,主线程树和impl线程树彼此是完全独立的。impl树实际上是主线程树的拷贝,尽管使用的是不同的数据结构。我们定期同步主线程树到impl树,这个过程我们称为"commit".commit是我们递归的遍历主线程树的layer,并“PushPropertiesTo”到impl树上的对应layer的过程。我们在主线程完全被阻塞的情况下,在impl线程运行commit。

何时执行commit的基本逻辑被延迟了。当主线程树发生变化时,我们只是简单记录下需要一个commit(setNeedsCommit)。当layer的内容发生变化时,比如,改变了HTML div text,这会被看作一次commit.稍后(在scheduler的裁决下,稍后讨论)我们决定执行commit,commit是阻塞操作但还是很经济的:通常不会超过几毫秒。

关于我们原始线程模型的一个题外话:我们假设主线程和impl线程都是messageloops.例如,这两个线程都有postTask和postDelayedTask原语。我们尽量使两个线程都处在idle状态,并尽量使用带锁的,阻塞线程的异步程序。

commit流程如下(具体实现见CCThreadProxy):

●主线程取得被破坏的区域。这会转换成一个setNeedsCommit.

●将setNeedsCommit信息传递给impl线程。

●impl线程将setNeedsCommit传递给CCScheduler.

注意,scheduler是impl部分的概念---scheduler不能访问任何的主线程状态。

●scheduler会考虑系统的整体情况(最近是否redraw过,下一帧什么时候开始,很多其他的事情),然后最后说“好的,开始一个commit”.

●scheduler发送的beginFrameAndCommit命令转换成一个postTask返回给主线程。

●当bFAC(beginFrameAndCommit)信息在主线程运行时,我们作如下的事情:

  □impl部分的scroll应用到主线程;

  □调用requestAnimationFrame回调;

  □Paint需要重新paint的layer(软件光栅化).

●当painting结束,我们发送一条信息给impl线程说“beginCommit”。然后主线程就等待“commit done”事件。impl线程完成commit之后会发送“commitdone”事件。

●impl线程的beginCommit信息上传textures,然后同步树结构。当这两件事情都完成,impl线程发送“commit done”事件,unblock之前阻塞在该线程上的主线程。这样commit过程就结束了,两棵树也同步了。

在这一点上,impl树可以在不与主线程协商的情况下,随时开始draw.类似地,主线程可以在不与impl线程协商的情况下,随时改变主线程树。

在CCThreadProxy架构中我们有一条非常重要的原则:主线程可以发起对impl线程的阻塞调用,但是impl线程不可以发起对主线程的阻塞调用。违背这条规则会导致死锁。

CCProxy

为了允许线程合成器的发展同时保留单线程合成器,我们需要在单线程和多线程模式下都能运行相同的基本的two-tree架构。

我们仍然是拥有两颗树和延迟commit,只是简单的运行一个不同的同步/调度算法,并在主线程持有树。这是通过CCProxy接口实现的.CCProxy接口抽象了主线程和impl线程之间交换的信息类型。

●setNeedsCommit:通知proxy调度一个主线程树的commit到impl线程树。

●setNeedsRedraw:通知proxy绘制impl线程树(不同步树)。

●setVisible:通知proxy设置impl线程树可见或不可见。

●compositeAndReadback(void* buf)

●...lots more

因此,CCProxy有两个继承类:

●CCSingleThreadProxy:单线程模式下运行合成器,impl线程树存在于主线程并在主线程中被绘制。

●CCThreadProxy:在另一个线程,impl线程,中运行合成器,这是”线程合成器“模式。

CCScheduler

除了同步树以外,合成器中还有很多逻辑要处理:什么时候commit,什么时候draw,什么时候运行动画,什么时候上传texture.等等。这些逻辑并不特定于impl运行于合成线程还是主线程,所以这些逻辑放在单独的一个类CCScheduler中。这个Scheduler逻辑上是impl tree的一部分,所以在线程模式中运行于impl线程中。Scheduler本身是很简单的类,粘合了两个关键系统:

●CCFrameRateController:决定何时是绘制的最佳时机。这个类监听底层操作系统的vsync api,以此来检测vsync的间隔,同时还处理

来自GPU的更新。它的职责是动态的选定一个目标帧率,并定期告诉scheduler"现在是绘制的好时机"。

●CCSchedulerStateMachine:跟踪合成操作的所有状态,比如,screen dirty,commit needed,commit begun,等等。我们尽量将所有的corner base逻辑都放在这部分代码中,这样,这部分就可以进行详尽的单元测试了。

InputHandling

合成线程的一个关键用途是即使在主线程阻塞的情况下也能平滑滚动网页,在input event到达主线程的event loop之前,将其打断并重定向到impl线程,我们可以做到平滑滚动网页。

在impl线程,event事件会到达WebCompositorInputHandler.这个handler接收event并请求impl线程树尝试滚动特定的layer.尽管如此,滚动有时会失败:WebKit没有为每一个可滚动的区域建立一个单独的layer(和一个与之相关联的clip object).因此,在impl树中,我们追踪每一个在impl这端不可滚动的layer区域。来自WebCompositorInputHandler的滚动请求如果因为落入不可滚动区域而失败的话,我们就按照正常流程发送scrolling event到主线程。我们将主线程处理的scroll称为“slow scrolls”,impl线程处理的scroll称为"fast scrolls".

memorymanagement

合成器是基于这样一个想法构建的:将layer的内容先缓存在texture上(或者其他gpu友好的representation)。当然,这需要使用内存。Chrome,作为一个基于标签的浏览器,有时会有几百个标签打开,我们需要在这些标签中间管理内存。

我们使用两级内存管理方案。在GPU进程中,我们有GpuMemoryManager跟踪所有可见的标签,以及与这些标签相关联的GraphicsContext。

粗略地讲,GpuMemoryManager根据可见性及最近使用频度来决定每个GraphicsContext该得到多少GPU资源。全局的memory manager也会影响每个标签请求的workload.所以一个大的gmail标签实际上得到的资源会比,比如,一个小的弹出窗口要多。

在合成器这一层,每个LayerTreeHost/Impl对都会从GPU进程分配到一定预算的内存。它们将尽量不超出这个内存预算。我们通过给组成所有Layer的所有tile排定优先级,并在达到内存界限时释放低优先级Tile的内存来使内存不超预算。优先级包括可见性,与viewport的距离,tile是否在动画层,当前层使tile上屏的速度等。

TextureUpload

关于这些texture的一个挑战在于:

我们是在render进程的主线程光栅化这些texture,但实际上需要把它们放入GPU内存中。

这就要求将这些texture(及它们的内容)的信息先传到impl线程,再传到GPU进程,最后再到达GL/D3D驱动中。如果按部就按的去做,会导致一遍又一遍的拷贝同一个texture.这是我们完全不想做的。

我们现在使用两种tricks来使这个过程快一点。

为了理解它们,先看如下两个概念”painting”VS”rasterization”

●Painting是指,通知Webkit将renderobject树的一部分备份到GraphicsContext.我们可以传入一个Painting例程实现的GraphicsContext,这样在GraphicsContext接收到命令时就会马上执行命令。或者我们也可以传入一个recordingcontext,在接收到命令时只简单记录下这些命令。

●Rasterazition是指,实际的执行Graphicscontext命令。我们通常在CPU上执行光栅化命令(softwarerendering),但也可以使用Ganesh在GPU上直接执行。

●Upload是指,我们将main memory中光栅化好的bitmap作为texture上传给GPU。

记住这些定义,我们使用如下trick处理textureupload:

●Per-tile painting:我们传给WebKitpaint一个recording context,只简单记录GraphicsContext操作到SKPicture数据结构中。然后我们从一个picture光栅化几个texture tile.

●SHM upload:不同于光栅化到renderer heap中,我们分配一块shared memory buffer并光栅化到这里。GPU进程使用这块shared memory buffer调用glTex*,避免了一次texture拷贝。

texture上传的制胜法宝是“zero copy” upload.在这种方案中,我们在render进程的sandbox中管理一个指向gpu memory的指针,我们直接软件光栅化到这块gpu memory上。

我们目前还没有实现,这是我们期望达到的。(译者注:chromium v35上已经实现)

Animation

我们允许动画添加到layer.表示layer位置和透明度虽时间变化的曲线,可以用来渐变和转换layer.尽管动画是在主线程被加入的,它们却在实现线程被执行。所以合成器完成的动画可以看成是"搭便车"(译者注:即在合成layer的过程中顺便完成的动画效果)。

Terminology

Threads:

●WebKit thread == Main thread.LayerChromium层次结构存在于此线程中。

●Compositor thread == 我们执行合成的线程。我们叫它impl线程,因为这是我们实现合成的地方。

●IO thread == 接收IPC的chromium线程。

Impl线程是我们经常使用的词。合成器既可以在单线程模式也可以在多线程模式下运行。impl线程只意味着“这存在于系统的impl那半部分”。

看到impl线程字样并不意味着代码只能运行在合成器线程--而是意味着它处理的是结构的impl那部分数据。

后缀表明了数据存在于哪个线程中:

●impl类存在于合成线程,比如,cclayertreehostimpl.

●没有后缀意味着数据存在与主线程,比如,cclayertreehost.

我们使用了通常来讲意义相近的词来表示在更新screen的过程中非常重要但又完全不同的步骤:

●Painting:向layer请求内容的过程。这是我们要求webkit告诉我们layer上有什么内容的过程。我们可能稍候将内容软件光栅化到bitmap上,或者我们可以do someting fancier.Painting是主线程操作。

●Drawing:个过程是将layer tree通过opengl smashing到屏幕上的过程。Drawing是impl线程操作。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值