Constellation Scorpius

Technical notes

Adding Files Into WebKit Xcodeproj in Linux

Old days, WebKit project supported 7 (or 8?) build systems: GYP, CMake, Makefile, waf’s wscript, Xcode’s xcodeproj, Qt’s .pro, and Visual Studio’s vcxproj. But the situation is significantly improved these days. Now there is only 2 build systems: CMake and Xcode.

Adding files into CMakeLists.txt is easy. Just adding filenames to CMakeLists.txt. So for Linux WebKit developers, only one barrier to add new files is adding files into xcodeproj without Xcode!

I created small scripts, adding files into JavaScriptCode.xcodeproj. This script is easily extended to support the other WebKit xcodeproj files. This tool uses slightly modified node-xcode for WebKit xcodeproj. And the code repository is here.

How to Build JavaScriptCore on Your Machine

TL;TR

1
Tools/Scripts/build-webkit --jsc-only

JSCOnly port

WebKit JavaScriptCore (JSC) is a high quality JavaScript engine that is used in many production-quality software. The performance of JSC is comparable to the other JavaScript engines like V8, and SpiderMonkey 1. And its spec conformance reaches 99% in ES6 compat-table 2.

While JSC is responsible to the JavaScript engine part in WebKit, the standalone JavaScript engine cannot be built easily. As a same to V8 and SpiderMonkey, JSC also provides the standalone shell to test itself. In OS X environment, the JSC shell can be built with the command Tools/Scripts/build-jsc without doing nothing before. However, in the other ports (EFL and GTK), we need to build WebKit’s dependencies despite these dependent libraries are not necessary to JSC. Building the dependent libraries takes much time since it includes many fundamental libraries, glib, gtk+, cairo, mesa and so on. In addition, once the dependent libraries are updated, which is noted in jhbuild.modules, we need to rebuild these libraries.

The dependent libraries become obstacles in many cases. For example, if you need to build the old revision of JSC to confirm the performance regression, we need to rebuild the old dependencies for that. Another case is testing on the other platform. If you need to build 32bit JSC on Linux, you may set up the LXC container for that. At that time, you need to build all the dependencies to build JSC on the test box even if those dependencies are not related to JSC.

To overcome this situation, we recently introduce a new port JSCOnly 34. The major goal of this port is the followings; (1) providing the platform-independent way to build JSC with significantly fewer dependencies and (2) providing the playground for new aggressive features. JSCOnly port only builds and maintains JSC and its dependent parts, such as WTF, and bmalloc. Since WTF, bmalloc, and JSC are well-written for the Nix environments, we have very few specific part to maintain this port. This port allow us to build JSC on any platforms with significantly less effort.

How to build

The standalone JSC shell in JSCOnly has only one dependent library (ICU), while building process requires some dependencies (like perl, ruby, python, bison, flex etc.). Before building JSC, we need to install these dependencies via your favorite package manager.

1
sudo apt install libicu-dev python ruby bison flex cmake build-essential ninja-build git gperf

Then, build your JSC shell. Of course, you need to check out the WebKit tree.

1
git clone git://git.webkit.org/WebKit.git

And then, build JSC.

1
Tools/Scripts/build-webkit --jsc-only

That’s it.

In addition to that, you can build the JSC shell with the static JSC library.

Updated according to the comment.

1
Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON -DUSE_THIN_ARCHIVES=OFF"

Reading the Paper, “ELI: Bare-Metal Performance for I/O Virtualization”

P.S.

あったほうがわかりやすいかもということで, 当初抜いていた簡易 Overview 再掲.

Note: This article is written in Japanese. In this article, we introduces the paper by A. Belay et al., “ELI: Bare-Metal Performance for I/O Virtualization” [Abel Gordon et al. ASPLOS ‘12].

この記事は, システム系論文紹介 Advent Calendar 2015 12/22 の記事として書かれました.

Reference

  • Abel Gordon, Nadav Amit, Nadav Har’El, Muli Ben-Yehuda, Alex Landau, Assaf Schuster, Dan Tsafrir. ELI: Bare-Metal Performance for I/O Virtualization, In Proc. of 17th international conference on Architectural Support for Programming Languages and Operating Systems (ASPLOS ‘12), 2012, pp 411-422.

Overview

VT-x, そして後の EPT 拡張によって, CPU, memory の virtualization は efficient に達成された. じゃああとは device ということで, VT-d によって, multiplexing はできないけれども, device によらず, isolation を壊さずに VM に device を efficient に assign することができた (pass-through).

とおもいきや, device が遅い場合がある. 調べたところ, interrupt が大量に入るような device ではなんと native 性能の 60% しかでてなかった. これは, VT-d の interrupt remapping は, 別に hypervisor を bypass して interrupt が入るわけではなく, interrupt は結局 VM exit して host に戻るという仕組みに存在した……

interrupt は実際は VM の管理 domain / host ではなくほとんど guest 向けのものになる. のならば, すべての interrupt を guest に直接渡してしまって, で, host に来るべきものだけ, host に戻せばいいのではないだろうか? しかし, guest の interrupt descriptor table (IDT) をそのまま使ってしまっては, host の受け取るべき interrupt を malicious な guest が乗っ取るかもしれない. この構図は, shadow page table の時の構図とほぼ同じだ.

ELI は IDT を shadowing し, shadow IDT を提供する. もちろんこの shadow IDT は guest からは直接触れない. host は guest IDT の変更を write protect で検知し(good old shadowing!), shadow IDT に反映する. VM exit を起こさず, guest の interrupt はすべて guest 内で処理される, Exit-Less-Interrupt (ELI) を提案する.

評価の結果, unmodified かつ untrusted guest が 97 - 100% の performance (compared to bare-metal) を達成した.

Background & Motivation

I/O は disk controller や NIC など, 仮想化環境において支配的な処理であり, direct device assignment (pass-through) の motivation となっている. I/O-intensive workloads においては, direct device assignment がなければ unacceptable な performance になるのだが, direct device assignment を行ったとしても bare-metal 環境と比較し, 60 - 65% 程度の performance しか出すことができない. direct device assignment はその I/O path において host の処理をほぼ全て取り除いているにも関わらずである (DMA remapping による host の介在しない DMA の発行, EPT による BAR の map による MMIO の direct access).

performance degradation は interrupt の処理で起きている.

interrupt handling

上の図の一番下が bare-metal. 対して一番上が現状の direct device assignment の時の interrupt の handle のされ方だ. 仮想化環境において, guest mode である場合,

  1. interrupt が起こるとまず VM exit が起きて host に戻る.
  2. そして host は hardware に interrupt completion を発行する.
  3. VM entry の際の virtual interrupt injection によって guest に interrupt を inject し resume.
  4. guest は物理マシンだと思っているので, 自分も interrupt completion を同様に発行する.
  5. guest の interrupt completion はまた VM exit を起こし host にもどる.
  6. host は interrupt completion の emulation をおこなって guest に resume.

ということが行われる. VM exit / entry の cost は non-I/O-intensive な workload にとってはそれほど問題がないが, I/O-intensive な workload + 近年の high performance storage / NIC にとっては, frequent な interrupt の生成により, これらは大きな問題となった.

これに対して, interrupt を減らすという手法をとる (例えば, polling や interrupt coalescing) こともできる.

  1. polling は OS handler の呼ばれる回数もコントロールできるが (batching すれば), batching する場合は latency が上がる. また, event がないときには CPU cycle を無駄にし, idle state になれないために power consuming でもある.
  2. interrupt と polling を組み合わせる hybrid な手法もある (interrupt 入ったらしばらく polling して… というやつ, NAPI では by default) が, dynamic に切り替える手法は実際には常にうまく行くというわけではない. その理由の一つは, 将来どれくらい interrupt が来るのか予測しなければならないという難しさだ (予測して interrupt / polling どちらのほうが cost が低いかを判断して切り替えるのだから).
  3. interrupt coalescing は, device に一定時間中の interrupt を coalescing して1回送らせる, もしくは一定回数の interrupt を coalescing して1回送らせるよう設定するものだ. これはもちろん latency が上がるという欠点があるが, それ以外に, 例えば TCP traffic の burst を引き起こしたりもする. coalescing の適切な parameter を求めるのは困難で, workload に依存する.

SR-IOV による asssignment ではいずれも高い CPU 利用率を bare-metal に対して示している. これについて, interrupt が大きなオーバーヘッドであることが demonstrate されている. 仮想化環境での interrupt の additional なオーバーヘッドを下げる研究についてもいくつか存在するが, hardware extension を要求する, Network device には適用できない (PV block device の virtual interrupt の coalescing を in flight な command 数に応じて行っている), polling を行うため前に述べた欠点がある, などの問題がある.

Design: ELI

cost となる 2つの exit (interrupt delivery と interrupt completion) を取り除くべく, この論文では ELI (Exit-Less-Interrupt) を提案する. 重要な点は, ある CPU core に来るほとんどすべての physical interrupt は, その CPU core で動いている guest に向けたもの だということだ. その理由は, high-performance を志向する guest は pinned な CPU core を専有しているであろうということ, そして I/O-intensive workload のために device assignment を SR-IOV を用いて行っていること, そして interrupt rate はふつうは実行時間に比例するということである.

Exitless Interrupt Delivery

仮想化の key は, ほとんどすべての時間を guest に使わせること. host の介在はなければないほどよい. ほとんどすべて guest への interrupt なのであれば, guest に interrupt を送ればよい.

ELI ではすべての physical interrupts を guest に送る (VT-x の制約上 all or nothing なので all しか選べない). そして, guest に関係のない, host のための interrupt は exit を起こして host に戻させる. この時, ELI は guest が trusted であったり, para-virtualized であったりといったことを求めない. ELI は unmodified かつ untrusted guest に対してこれを実現する.

ELI

guest が unmodified かつ untrusted であっても安全に host の interrupt を exit できるようにするために, ELI は Interrupt Descriptor Table (IDT)1 を shadowing し, shadow IDT を作成する. shadow IDT はおなじみ shadow page table に非常に近い. guest は自身の guest IDT が設定されていると思っているが, 実際には shadow IDT が設定されている. shadow IDT は guest IDT と in sync である.

上の Figure は ELI の interrupt delivery flow を示している. physical interrupt はすべて guest に exit なく伝えられ, shadow IDT によって assigned device については exit なく guest によって処理が行われる. 一方本来 host に伝えられるべきであった interrupt は, exit を起こすことによって guest を抜け, host にて処理がなされる.

shadow IDT は guest が通常触らない(普通のメモリとして使わない)が IDT として指定可能な空間である device PCI BAR に余分にページを増やし, そこに設置される2. shadow IDT は host に割り当てられた interrupt については non-present 状態にしておく. すると, interrupt が送られると NP exception が起こり, VM exit が発生する.

guest IDT は write-protect な状態に置かれ, 変更を検知し, shadow IDT へ反映する. これは shadow paging の手法とコンセプトは同じ. shadow IDT も guest による modify を避けるため write-protect 状態に置かれる3.

Exitless Interrupt Completion

もうひとつの exit, interrupt completion も取り除きたい. interrupt completion は EOI LAPIC register に書き込むことによって行われる. old interface では LAPIC area の一部に存在し, ほぼすべての LAPI access は exit を起こすが, 新しい x2APIC では MSR (Machine Specific Register) に書き込むことができる. MSR はそれぞれの MSR ごとに細粒度で exit するかどうかを control することができる.

そこで ELI では EOI x2APIC MSR を exit を起こさないで書き込めるように設定し guest の direct access を許可する. これによって interrupt completion についても exit を起こさずにすむ4.

Evaluations

筆者らは KVM 上に ELI を実装し評価を行っている. また性能向上のために host では huge pages を有効にしている. 確認したいのはそもそもの発端, I/O-intensive な workload における bare-metal に対する throughput や latency だ.

Throughput

Bare-metal に対する Throughput を示している. それぞれ Netperf TCP stream の write, Apache に向かって ApacheBench を用いて評価したもの, Memcached に向かって libmemcached の Memslap bench を (get 90%, set 10%) で実行したものだ.

それぞれの ELI の機能の contribution によって throughput は改善し, full ELI ではそれぞれ 98%, 97%, 100% の throughput (compared to bare-metal) を示している. interrupt delivery のほうが interrupt completion よりも寄与が大きいのは, host が interrupt を handling する処理のほうが複雑で, それが取り除かれたためであるとのこと.

Latency

次に, Netperf UDP の ping-pong を行い, その latency を評価する. Baseline (ELI なし) が bare-metal の 129% の高い latency を示しているのに対して, full ELI では 102% にまで改善している.

Conclusion

x86 仮想化が direct device assignment において interrupt の処理に host の介在が必要であることが high-performance storage and NIC にとって overhead となることを示し, ELI (shadow IDT, x2APIC MSR) の techniques を用いることによって, assigned device の interrupt の処理における VM exit を除去し, bare-metal throughput & latency を達成した. また, それを unmodified かつ untrusted guest に対して可能にした5.

Thoughts

問題が鮮やかで, 解法も従来の仮想化の手法を応用し, かつ特に何も犠牲にしておらず, 非常に堅実なものに思える. まあ動くからいいでしょ… としておいたものが, 環境が変わって (high-performance storange / NIC の登場など) 問題となるケース, かっこいいですね.

  1. IDT は interrupt の番号とそれぞれの handler をひも付ける

  2. IDT は kernel address space に map されている必要がある. map され, map され続け, かつ通常アクセスしない場所として device の PCI BAR に余分なページを増やしてそこに設置する. Pass-through の際, 例えば Xen では仮想 device を作る. 仮想化された PCI configuration space に元の device と同じ設定を置いておき, EPT を用いて host に見えている BAR の host physical address と, guest における guest physical address をひも付けて MMIO の direct access を許可する. この時, typical には (Linux / Windows) pci resource の大きさを query しそのサイズ全域を kernel address space に map する. ので, ここで例えば, 本来 1MB のものを 2MB だと宣言しておき, 後ろ 1MB を device の BAR ではない普通の memory に EPT でひも付けておけば, そこを shadow IDT として利用可能だ.

  3. EPT の該当エントリを read only に設定しておけばいけるか.

  4. じゃあなんで今までからしなかったのと言われれば, もちろん問題があったからだ. guest には例えば virtual な keyboard によって生成され inject された virtual interrupt と assigned device による physical interrupt の区別がつかない. しかし, virtual interrupt の場合は, EOI は host の emulation に向かって行われるべきで, physical な EOI x2APIC MSR に書き込まれるのは問題である. そこで, ELI では virtual interrupt を inject するときには injection mode という mode に fallback する. host が virtual interrupt を入れるとき, ELI は guest VM を injection mode にしてから inject する. injection mode は guest の IDT を shadow IDT から guest IDT に戻し, 代わりに interrupt の exit や EOI MSR 書き込みの exit を有効にする. 早い話が, ELI なしの普通の状態に一時的に戻す. すると virtual interrupt は普通に今までの方法で処理される. そして virtual interrupt の EOI を exit によって無事受け取ったらまた guest VM を ELI の mode に戻す.

  5. security についてはもう少しあったが補足で. 例えば guest が interrupt を disable にしたままにしてしまったら interrupt で exit しない以上 timer 割り込みも exit を起こさず, guest が走ったままになってしまう. これについては x86 virtualization の preemption timer を用いれば, unconditional exit を起こすことができる.

Gave a Talk About CSS JIT

Once again, I had a chance to talk about CSS Selector JIT at x86/x64 optimization meetup. And I gave a presentation that is updated from the previous one.

Updated slides include the following differences.

  • Example flow of JIT compiled CSS Selector state machine
  • Rough performance evaluation with Dromaeo cssquery benchmark