Atomの起動速度がべらぼうに遅い原因を調査してみた

mainvisual

※ 2016/6/26 編集:
  • 文章が単調だったので、記事を書いた背景を追加した上で校正しました

自分のお気に入りのエディターはAtomです。 私は、「オープンソース大好き人間」なので、なんでもオープンソースでないと気がすみません! 市販のやつも確かにいいけれど、不良動作のエラーがあったとき、 どんなことが起こっているのかを確認したい時もあるんです

それに、自分でカスタマイズできるのがいいですね!気に入ったプラグインがあれば、 それを入れるだけでどんどん機能を追加して、自分の使いやすいようにできますし!

ただ、AtomはNodeエンジンとJavascriptで書かれたエディタなので、 バイナリで動くエディタと比べて、動作が遅いです…

それに加えて、メモリをバカみたいに食うのが、このエディタの残念なところ。

アクディビティモニタスクリーンショット

1.57GBかぁ… ちょっと、多すぎますね

今回の主題

  • 今回は、このAtomくんを対象にして、どこがボトルネックになっているのか、その原因を調査します。
  • 原因が簡単であれば、C++で書き直したりして、解決します。

その前に、Atomの特徴を…

その前に、初めてこの記事を見に来た人に、Atomとは何かをざっくりと説明したいと思います。(知っている人は、読み飛ばしてもらって大丈夫です!)

Atomには、次のような特徴があります。

  • オープンソースである
  • プラグインがcoffeescript(javascript)なので、作りやすい
  • 見た目が綺麗
  • プロジェクトごとに管理できる。
  • 設定を複数のコンピュータで同期できる
  • Linuxでも動く、マルチぷらっとフォーム
  • 設定方法がわかりやすい
  • Atomは沢山のプラグインの集まりであること

ですが、メモリーの使用量がとても大きく、起動時間がべらぼうに遅い….
といった欠点もあります。

そこで、この起動を遅くしている原因はなんなのかの原因を調査しました! 結果、まだ原因にはたどり着くことはできませんでした。

しかし、原因の解決策の1つとして、C++で書き直すという手があります。

起動時間の調査

Atomには、たくさんの機能が詰め込まれているので、 まずは、なにが遅いのかがわからなければ手の打ちようがありません!

そこで、どのプラグインにどれだけの時間がかかっているのかを確認しましょう!

まず、Macの場合、次のような操作をすることで、確認できます。

  • 「shift-command-p」を同時に押して、コンソールを立ち上げます。
  • テキストエリアに、「timecop」と入力します。
  • 「Enter」を押します。

windowsの場合は… 自分で調べてくれると助かります。

自分の環境では、起動に「3243ms」かかっていることがわかりました。 起動に時間がかっているプラグインが順に表示されています。

「Emmet」が1秒以上もかかっていることがわかりました。(これをなんとかしたい…) ですので、「Emmet」が遅い原因は何で、それを改善する方法はないか探しました。

Emmetの調査

EmmetはHTMLやCSSを爆速で書くためのプラグインです。
ですので、自分の環境では欠かせません。

Emmetのリポジトリはこちらです。

emmetの方は、プラグインのコアです。
それを、atom用にラップしたのが、emmet-atomです。

ですので、emmetのコアの方を調査することにしました。

と思いましたが、実際に高速化、省メモリー化する手段がなければどうしようもありません。 まずは、どのように高速化をするべきかを検討することにしました。

C++でnodeのアドオンを製作する

googleで「node js c++ アドオン」と調べることによって、次のような記事をみつけました。

これによると、


まずは、試し。実際にどのように動作するか、テストしてみます。  
実際に試した方が、今後応用も効きますし、何よりそのぐらい知ってないとむしゃくしゃします。

 ということで、実際に試してみました

まず、ローカル環境で

```bash
$ npm install -g node-gyp

を実行

次のような画面が表示されて、node-gypがインストールされていることを確認します。

$ node-gyp
  Usage: node-gyp <command> [options]

  where <command> is one of:
    - build - Invokes `make` and builds the module
    - clean - Removes any generated build files and the "out" dir
    - configure - Generates a Makefile for the current module
    - rebuild - Runs "clean", "configure" and "build" all at once
    - install - Install node development files for the specified node version.
    - list - Prints a listing of the currently installed node development files
    - remove - Removes the node development files for the specified version

node-gyp@3.3.1  /usr/local/lib/node_modules/node-gyp
node@5.8.0

次に、適当なディレクトリを作成します。

mkdir nodejs-addon-sample && cd $_

次に3つファイルを作成します。これは、必要最低限の構成です。

  • c++のソースコードファイル
  • nodejsで呼び出すためのjavascriptソースコードファイル
  • node-gypでビルドするための設定ファイル

です。

c++のソースコードファイル

これは実際、C++であればなんでもいいです。
拡張子はcpp でも、 ccでもOKです。

今回は、hello.ccという名前にしました。

そして、最小のc++のコードはこちらです。

// hello.cc
#include <node.h>

namespace demo {
  void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
    v8::Isolate* isolate = args.GetIsolate();
    args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "world"));
  }

  void init(v8::Local<v8::Object> exports) {
    NODE_SET_METHOD(exports, "hello", Method);
  }

  NODE_MODULE(addon, init)
}

これは、nodejs側から、helloというメソッドが呼ばれた場合に、"world"という文字列を返すメソッドを作成しています。

このソースコードで理解すべきポイントは次に3つです。

  1. args.GetReturnValue().Set() というメソッドで、node側に文字列やオブジェクトなどを返す。
  2. void init(v8::Local<v8::Object> exports) でc++側の関数と、nodejs側の関数の関係をバインドする。
  3. NODE_SET_METHOD(exports, "<node側で呼び出す関数の名前>",<c++でのメソッド>); の以上です。

あとは、クラスを作ったり、処理を書いたりして、node側に返してあげるだけで、アドオンが出来上がります。

ですが、事はそう単純ではありません。

javascriptでは、動的型付け。c++では静的型付けです。 ですので、javascriptで渡された型が、ちゃんとC++側の型に沿っているかをチェックしなければなりません。

またh、変数の個数が、ちゃんと満たされているかのチェックも必要です。

それを含めると、次のようなコードになります。

#include <node.h>

namespace demo {

using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Check the number of arguments passed.
  if (args.Length() < 2) {
    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Wrong number of arguments")));
    return;
  }

  // Check the argument types
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Wrong arguments")));
    return;
  }

  // Perform the operation
  double value = args[0]->NumberValue() + args[1]->NumberValue();
  Local<Number> num = Number::New(isolate, value);

  // Set the return value (using the passed in
  // FunctionCallbackInfo<Value>&)
  args.GetReturnValue().Set(num);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(addon, Init)

}  // namespace demo

この2つの処理なのですが、必ず個数チェックを行ったあとに型チェックを行いましょう。
でないと、```args[1]```が存在しなかった時は、c++側で落ちます。


ちなみに、```args[0]->IsNumber()```の部分ですが、IsNumberだけではなくて、次のようないろいろな種類があります。

* args[n]->IsNumber()
* args[n]->IsString()
* args[n]->IsFunction()
* args[n]->IsObject()
* args[n]->IsUndefined()

また、数値を返す ```args[n]->NumberValue()``` や オブジェクトを返す ```args[n]->ToObject()``` などもあります。


#### nodejsで呼び出すためのjavascriptソースコードファイル

これはもう簡単です。  
いきなり、ソースコードを書きます

```javascript
// main.js
var addon = require('./build/Release/addon');

console.log(addon.add(10, 20));

ソースをビルドすると、./build/Release/addonにバイナリファイルができるので、それを読み込んで実行しているだけです。

node-gypでビルドするための設定ファイル

これも簡単なので、いきなりコードを書きます

# building.gyp
{
    "targets": [
        {
            "target_name": "addon",
            "sources": [ "hello.cc" ]
        }
    ]
}

### ついにビルド

以上3つのファイルを1つのディレクトリにまとめます。

```bash
$ ls
binding.gyp    hello.cc    test.js

そして、次のコードを実行

$ node-gyp configuregyp info it worked if it ends with ok
gyp info using node-gyp@3.3.1
gyp info using node@5.8.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [ '/usr/local/lib/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/k4zzk/Desktop/npm-module-test/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/usr/local/lib/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/k4zzk/.node-gyp/5.8.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/k4zzk/.node-gyp/5.8.0',
gyp info spawn args   '-Dnode_gyp_dir=/usr/local/lib/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/k4zzk/Desktop/npm-module-test',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.' ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  CXX(target) Release/obj.target/addon/hello.o
  SOLINK_MODULE(target) Release/addon.node
gyp info ok build

このようになれば、ビルド成功です

$ ls
binding.gyp build       hello.cc    test.js

これから、EmmetのC++化を頑張ろうかと思っていたのですが、今日が終わってしまったのでまた今度にします。 ではまた。

もっと知りたいなと思ったときは!

その他、この記事でわからないことがあったら、teratialというサービスを利用するといいと思います。 勇者が初歩的な質問からマニアックな質問に幅広く答えてくれますよ! 是非、一度使ってはいかがでしょうか?