Hatena::ブログ(Diary)

by edvakf in hatena

2010-12-07

node.js のソースぐらい読んでおきたい!

JavaScript Advent Calendar 2010 という企画をやっています。既にもう7日目なのですが、まだまだ os0x さんや hasegawayosuke さんや nanto_vi さんや secondlife さんといったすごい方々が記事を書いてくれる予定になっていますので、是非チェックしてみてください。


今日は、最近話題の node.js を読んでみます。僕自身は node.js を追っかけてたのは今年の5月ぐらいで、ソースは半年以上見てなかったのですが、この機会にまた読みました。この記事は、C++ は一応読めるけど V8 とか libev はあまり知らない node.js 好きの人を念頭に置いています。

拙訳の Embedder’s Guide - V8 JavaScript Engine に書いてあるようなことは説明なしでいきたいと思います。また、適宜 libevlibeio のドキュメントや、誰かが公開してる V8 のドキュメント (Doxygen で生成したやつ) も参考にしていこうと思います。

node.js のソースは Ryan Dahl さんの github ページにあります。

main 関数があるのはその中の src/node_main.cc です。main はたったこれだけ。

// Copyright 2010 Ryan Dahl <ry@tinyclouds.org>

#include <node.h>

int main(int argc, char *argv[]) {
  return node::Start(argc, argv);
}

src/node.cc にある Start 関数を呼んでいます。(前はこっちが main だった)

これから src/node.cc を見ていきます。


イベントループ

適当に省略しながら引用します。まずここで libev のイベントループを初期化します。あまりよくわかってませんが、ざっくり言えばイベントループというのは無限ループです。ループの最中にあるスイッチがオンになれば、次のイテレーションで特定の操作をすることになります。(参考:Linux Programming、epollの話

int Start(int argc, char *argv[]) {
  …

  // Initialize the default ev loop.
#if defined(__sun)
  // TODO(Ryan) I'm experiencing abnormally high load using Solaris's
  // EVBACKEND_PORT. Temporarally forcing select() until I debug.
  ev_default_loop(EVBACKEND_POLL);
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
  ev_default_loop(EVBACKEND_KQUEUE);
#else
  ev_default_loop(EVFLAG_AUTO);
#endif

  ev_prepare_init(&node::prepare_tick_watcher, node::PrepareTick);
  ev_prepare_start(EV_DEFAULT_UC_ &node::prepare_tick_watcher);
  ev_unref(EV_DEFAULT_UC);

  ev_check_init(&node::check_tick_watcher, node::CheckTick);
  ev_check_start(EV_DEFAULT_UC_ &node::check_tick_watcher);
  ev_unref(EV_DEFAULT_UC);

  ev_idle_init(&node::tick_spinner, node::Spin);

  …

この prepare_tick_watcher とか check_tick_watcher とかは構造体です。この構造体が PrepareTickCheckTick という関数に渡されることになります。ev_prepare_***ev_check_*** というのは、各イテレーションの最初と最後にそれぞれ実行されるコールバックをセットするものです。PrepareTickCheckTick は両方とも Tick という別の関数を呼んでいるだけなので、それを見ます。

static void Tick(void) {
  // Avoid entering a V8 scope.
  if (!need_tick_cb) return;

  need_tick_cb = false;
  ev_idle_stop(EV_DEFAULT_UC_ &tick_spinner);

  HandleScope scope;

  if (tick_callback_sym.IsEmpty()) {
    // Lazily set the symbol
    tick_callback_sym =
      Persistent<String>::New(String::NewSymbol("_tickCallback"));
  }

  Local<Value> cb_v = process->Get(tick_callback_sym);
  if (!cb_v->IsFunction()) return;
  Local<Function> cb = Local<Function>::Cast(cb_v);

  TryCatch try_catch;

  cb->Call(process, 0, NULL);

  if (try_catch.HasCaught()) {
    FatalException(try_catch);
  }
}

need_tick_cb とか tick_spinner というのはちょっと無視します。ループの終了条件に関係あるようですが、なんか dirty hack 的なことがコメントに書いてあります。

長ったらしいですが、単に process オブジェクト_tickCallback というメソッドを実行する処理になっています。JS で書くなら process["_tickCallback"].call(process); という感じです。_tickCallback は src/node.js で定義されています。

var nextTickQueue = [];

process._tickCallback = function () {
  var l = nextTickQueue.length;
  if (l === 0) return;

  try {
    for (var i = 0; i < l; i++) {
      nextTickQueue[i]();
    }
  }
  catch(e) {
    nextTickQueue.splice(0, i+1);
    if (i+1 < l) {
      process._needTickCallback();
    }
    throw e; // process.nextTick error, or 'error' event on first tick
  }

  nextTickQueue.splice(0, l);
};

というわけで、キューに溜まった処理を(エラーが出ない限り)全部やるということでした。エラーが出た場合は…大して面白くないので省略。


GC

src/node.cc の Start 関数に戻ります。

  …

  ev_check_init(&node::gc_check, node::Check);
  ev_check_start(EV_DEFAULT_UC_ &node::gc_check);
  ev_unref(EV_DEFAULT_UC);

  ev_idle_init(&node::gc_idle, node::Idle);
  ev_timer_init(&node::gc_timer, node::CheckStatus, 5., 5.);

  …

ここらへんは GC 関係です。

ev_idle_*** というのは、イベントループが忙しくないときに実行される処理を指定します。ev_timer_*** は指定した秒数(ここでは5秒)後に何かを実行するというやつです。ev_check_ のほうは start してますが、ev_idle_ev_idle のほうはどちらも init しただけで start してません。

Check という処理をまず見ます。ev_check_*** なので、イテレーションの最後に実行されるやつです。

// Called directly after every call to select() (or epoll, or whatever)
static void Check(EV_P_ ev_check *watcher, int revents) {
  assert(watcher == &gc_check);
  assert(revents == EV_CHECK);

  tick_times[tick_time_head] = ev_now(EV_DEFAULT_UC);
  tick_time_head = (tick_time_head + 1) % RPM_SAMPLES;

  StartGCTimer();

  for (int i = 0; i < (int)(GC_WAIT_TIME/FAST_TICK); i++) {
    double d = TICK_TIME(i+1) - TICK_TIME(i+2);
    //printf("d = %f\n", d);
    // If in the last 5 ticks the difference between
    // ticks was less than 0.7 seconds, then continue.
    if (d < FAST_TICK) {
      //printf("---\n");
      return;
    }
  }

  // Otherwise start the gc!

  //fprintf(stderr, "start idle 2\n");
  ev_idle_start(EV_A_ &gc_idle);
}

StartGCTimer というのは ev_timer_start(EV_DEFAULT_UC_ &gc_timer); をやるだけです。gc_timer というのは、さっき init した5秒カウントのやつでした。下の部分は、もし「忙しくなければ」アイドリングを開始するということです。

まず gc_idle に関連付けられた Idle 関数を見ます。

static void Idle(EV_P_ ev_idle *watcher, int revents) {
  assert(watcher == &gc_idle);
  assert(revents == EV_IDLE);

  //fprintf(stderr, "idle\n");

  if (V8::IdleNotification()) {
    ev_idle_stop(EV_A_ watcher);
    StopGCTimer();
  }
}

IdleNotification というのは V8 に「今なら GC してもいいよ」という通知を出します。V8 のドキュメントによると、もし GC されるべきオブジェクトが残ってなかったら、もう IdleNotification を呼ばないでいいよという意味で true を返します。おそらく、一回の GC で全部掃除すると処理待ちが気になってしまうため、何回かに分けて呼んでやるのだと思います。

最後に、gc_timer に関連付けられた CheckStatus 関数を見てみます。

static void CheckStatus(EV_P_ ev_timer *watcher, int revents) {
  assert(watcher == &gc_timer);
  assert(revents == EV_TIMEOUT);

  // check memory
  size_t rss, vsize;
  if (!ev_is_active(&gc_idle) && OS::GetMemory(&rss, &vsize) == 0) {
    if (rss > 1024*1024*128) {
      // larger than 128 megs, just start the idle watcher
      ev_idle_start(EV_A_ &gc_idle);
      return;
    }
  }

  double d = ev_now(EV_DEFAULT_UC) - TICK_TIME(3);

  //printfb("timer d = %f\n", d);

  if (d  >= GC_WAIT_TIME - 1.) {
    //fprintf(stderr, "start idle\n");
    ev_idle_start(EV_A_ &gc_idle);
  }
}

これは、メモリが少なくなってたり、前の Tick 終了から一定時間以上たってたらまたアイドリング(=GC)を開始するという意味です。


IOスレッド

Start 関数に戻ります。

ここでは、さっき見たメインスレッドとは別に libeio による EIO スレッドを起動しています。I/O に関することを全部 EIO スレッドに任せることによって、メインのほうのスレッドはブロックされなくなります。node.js っぽくなってきましたね。

  // Setup the EIO thread pool
  { // It requires 3, yes 3, watchers.
    ev_idle_init(&node::eio_poller, node::DoPoll);

    ev_async_init(&node::eio_want_poll_notifier, node::WantPollNotifier);
    ev_async_start(EV_DEFAULT_UC_ &node::eio_want_poll_notifier);
    ev_unref(EV_DEFAULT_UC);

    ev_async_init(&node::eio_done_poll_notifier, node::DonePollNotifier);
    ev_async_start(EV_DEFAULT_UC_ &node::eio_done_poll_notifier);
    ev_unref(EV_DEFAULT_UC);

    eio_init(node::EIOWantPoll, node::EIODonePoll);
    // Don't handle more than 10 reqs on each eio_poll(). This is to avoid
    // race conditions. See test/simple/test-eio-race.js
    eio_set_max_poll_reqs(10);
  }

  …

ev_async_*** は別スレッドと通信するためのものです。DoPollWantPollNotifierDonePollNotifier はとりあえず無視して、まず下のほうの eio_init(node::EIOWantPoll, node::EIODonePoll); を見ます。これは、EIO スレッドがメインスレッドの注意を引きたい時に EIOWantPollEIODonePoll を呼ぶという意味です。これらはメインスレッドのほうで実行されます。

// EIOWantPoll() is called from the EIO thread pool each time an EIO
// request (that is, one of the node.fs.* functions) has completed.
static void EIOWantPoll(void) {
  // Signal the main thread that eio_poll need to be processed.
  ev_async_send(EV_DEFAULT_UC_ &eio_want_poll_notifier);
}

static void EIODonePoll(void) {
  // Signal the main thread that we should stop calling eio_poll().
  // from the idle watcher.
  ev_async_send(EV_DEFAULT_UC_ &eio_done_poll_notifier);
}

コメントを読めば分かりますが、EIO スレッドが poll という操作をしたいときと、それが終わったときに呼ばれます。ev_async_send は他のスレッドのイベントループを起こす合図です。ここでさっきの WantPollNotifierDonePollNotifier を呼びます。これらは EIO スレッドのほうで実行されます。

// Called from the main thread.
static void WantPollNotifier(EV_P_ ev_async *watcher, int revents) {
  assert(watcher == &eio_want_poll_notifier);
  assert(revents == EV_ASYNC);

  //printf("want poll notifier\n");

  if (eio_poll() == -1) {
    //printf("eio_poller start\n");
    ev_idle_start(EV_DEFAULT_UC_ &eio_poller);
  }
}

static void DonePollNotifier(EV_P_ ev_async *watcher, int revents) {
  assert(watcher == &eio_done_poll_notifier);
  assert(revents == EV_ASYNC);

  //printf("done poll notifier\n");

  if (eio_poll() != -1) {
    //printf("eio_poller stop\n");
    ev_idle_stop(EV_DEFAULT_UC_ &eio_poller);
  }
}

eio_poll は、EIO スレッドのほうで処理待ちになってる(I/O 処理が終わった後の)コールバックを実行するためのものです。-1 は、全部の処理待ちのコールバックを実行できなかったのでまた poll したいという意味です。(あまり大量にやるといけないっぽくて、一回に10処理までに抑えられてる)

非同期処理用のライブラリを書くときはこの EIO スレッドを使います。


スクリプトの実行

Start 関数に戻ります。こんな感じで JavaScript のグローバルコンテクストとなるオブジェクトを作ります。

  V8::Initialize();
  HandleScope handle_scope;

  V8::SetFatalErrorHandler(node::OnFatalError);

  …

  // Create the one and only Context.
  Persistent<v8::Context> context = v8::Context::New();
  v8::Context::Scope context_scope(context);

  atexit(node::AtExit);

  // Create all the objects, load modules, do everything.
  // so your next reading stop should be node::Load()!
  node::Load(argc, argv);

  …

この Load 関数は長いのですが、色んなオブジェクトを初期化した後で src/node.js を読み込みます。src/node.js は一つの関数を返すので、それを f として、実行しています。だいぶ省略してますがこんな感じ。

static void Load(int argc, char *argv[]) {
  …

  // Compile, execute the src/node.js file. (Which was included as static C
  // string in node_natives.h. 'natve_node' is the string containing that
  // source code.)

  // The node.js file returns a function 'f'

  TryCatch try_catch;

  Local<Value> f_value = ExecuteString(String::New(MainSource()),
                                       String::New("node.js"));
  if (try_catch.HasCaught())  {
    ReportException(try_catch, true);
    exit(10);
  }
  assert(f_value->IsFunction());
  Local<Function> f = Local<Function>::Cast(f_value);

  // Now we call 'f' with the 'process' variable that we've built up with
  // all our bindings. Inside node.js we'll take care of assigning things to
  // their places.

  // We start the process this way in order to be more modular. Developers
  // who do not like how 'src/node.js' setups the module system but do like
  // Node's I/O bindings may want to replace 'f' with their own function.

  // Add a reference to the global object
  Local<Object> global = v8::Context::GetCurrent()->Global();
  Local<Value> args[1] = { Local<Value>::New(process) };

  f->Call(global, 1, args);

  if (try_catch.HasCaught())  {
    ReportException(try_catch, true);
    exit(11);
  }
}

src/node.js の一番最後で、引数に渡されたファイルを実行したり(module.runMain)、コンソールに入ったり(module.requireNative('repl').start())しています。

if (process.argv[1]) {
  // Load module
  if (process.argv[1].charAt(0) != "/" && !(/^http:\/\//).exec(process.argv[1])) {
    process.argv[1] = path.join(cwd, process.argv[1]);
  }
  // REMOVEME: nextTick should not be necessary. This hack to get
  // test/simple/test-exception-handler2.js working.
  process.nextTick(module.runMain);

} else if (process._eval) {
    // -e, --eval
    var indirectEval= eval; // so the eval happens in global scope.
    if (process._eval) console.log(indirectEval(process._eval));
} else {
    // REPL
  module.requireNative('repl').start();
}

イベントループの開始と終了

また src/node.cc の Start 関数に戻ります。いよいよメインスレッドと EIO スレッドをスタートします。

// TODO Probably don't need to start this each time.
  // Avoids failing on test/simple/test-eio-race3.js though
  ev_idle_start(EV_DEFAULT_UC_ &eio_poller);

  // All our arguments are loaded. We've evaluated all of the scripts. We
  // might even have created TCP servers. Now we enter the main eventloop. If
  // there are no watchers on the loop (except for the ones that were
  // ev_unref'd) then this function exits. As long as there are active
  // watchers, it blocks.
  ev_loop(EV_DEFAULT_UC_ 0);

コメントにあるように、ev_loop が無限ループとなり、もしキューされているイベントが何もなくなったらループから抜け出します。

最後に exit イベントを emit して終了です。(node.js で process.on('exit',function(){}); とできる)

  // process.emit('exit')
  Local<Value> emit_v = process->Get(String::New("emit"));
  assert(emit_v->IsFunction());
  Local<Function> emit = Local<Function>::Cast(emit_v);
  Local<Value> args[] = { String::New("exit") };
  TryCatch try_catch;
  emit->Call(process, 1, args);
  if (try_catch.HasCaught()) {
    FatalException(try_catch);
  }

  …
}

というわけで

ブラウザもこんなふうにイベント管理をしているみたいなので、なんとなく分かることができてよいかと思います。FirefoxのsetTimeoutの実装がとても参考になります。ブラウザのソースは大きくてどこを読んだらいいかわからないので、node.js はちょうどいい規模ですね。(基本は node.cc だけで、その他は非同期ファイルアクセスや非同期 DNS 解決や EventEmitter などの便利機能なので)

これから非同期 IO ライブラリの書き方の説明をしようと思いましたが、長くなったのでここで終りにして、また近いうちに別のところに書きます。


JavaScript Advent Calendar なのにほぼ C++ でしたね。確信犯です。

それでは明日からもまた JavaScript Advent Calendar をお楽しみください。

Happy Christmas!

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証