goonのクエリをチューニングした時のメモ

App Engine(Go 1.9)でDatastoreからデータを取得するのにgoonGoon.Runを使っていたが、プロパティを追加する改修*1をした後、 latencyが遅くなり、DatastoreでDeadline exceededが頻繁に発生するようになってしまった。

とりあえず該当するリクエストをStackdriver Traceで見てみるとdatastore_v3.Nextが7回ほど呼ばれていることがわかった。

まずはこの呼び出し回数を減らせば速くなるだろうと思い、Query.BatchSizeを設定してみたが、呼び出し回数に変化は無く、全く効果がなかった。
それも変だなと思い、試しにデバッグログを仕込んで1回の呼び出しでどれくらいのデータを取得できているのか見てみたところ、なんと90件くらいしか取得できていなかった。

ここまで改修前の状態を確認していなかったので念のため調べてみると、datastore_v3.Nextの呼び出し回数は5回、1回あたりのデータ取得件数は約120件だった。
このことから、原因はプロパティを追加したことでデータのサイズが増え、一度に取得できる件数が減ったことによるDatastoreへのアクセス増ということがわかった。

ということで次はMemcacheを使ってDatastoreへのアクセスを減らすようにしてみた。
方法としては今までのクエリにQuery.KeysOnlyを設定してキーだけを取得するようにし、本体はGoon.GetMultiで取ってくるというシンプルなもの。
しかし、これだと1000件のデータを取得しようとするとGoon.GetMultiがMemcacheにデータを格納する際に大量のメモリを消費してしまうため、エラーになったり、インスタンスが落ちるようになってしまった。
そこでGoon.GetMultiの呼び出しが100件ずつになるようにしてみたところ、エラーは解消されたように見えたが、どうやらMemcacheにデータが乗っておらず、常にDatastoreにアクセスしてしまっていた。
仕方なく50件まで減らしたら安定してキャッシュが効くようになり、ようやく速度が改善されるようになった。

そもそもデータ構造がイマイチっぽいので本来はそこから手を入れるのが良さそうだが、今からそれをやるのもなかなかに厳しい状況なので当分はこれでしのぐことになりそう。

*1:後で確認したらデータサイズが80%ほど増加していた...

第1回 ゴリラ.vimが開催されました

2/18(月)にゴリラ.vim #1が開催され、スタッフ兼発表者として参加してきました。

gorillavim.connpass.com

スタッフになったのはvim-jpのslackでゴリラさんこと@gorilla0513さんがVimの勉強会をやろうとしているのを見かけ、これは前々から思っていた渋谷でVim勉強会をするチャンスなのでは!?と会場提供を申し出たのがきっかけです。

そして当初は発表するネタが思い浮かばなかったのでスタッフに専念するつもりでいたのですが、少し経っても発表者枠が空いており、かつメンバーがVimConf 2018のスピーカーばかりという状態だったため、これは自分も何か話した方が良いんじゃないかと考え、ここ最近使ったgotypego-treeを基にVimと外部コマンドという発表をしてきました。

ちなみに発表・デモは@thincaさんのshowtimequickrunを使いました。

gorillavim.connpass.com

全体の発表内容としては、短い時間ながらも初心者向けから上級者向けまであり、とても充実したものとなっていました。
特に前半の発表は自分にもそういう時期があったんだよなぁ、と非常に懐かしい気持ちになりました。

その後、一通り発表が終わった後は懇親会となり、各々がVimトークでとても盛り上がっていました。

開催前からこのイベントは盛り上がる...という予感はありましたが、予想以上に盛り上がり、スタッフとしても発表者としても参加者としても、嬉しい限りです。

ということで、今回ご参加いただいた皆様、ありがとうございました。
次回も是非よろしくお願いします。
(#2は少し枠が増えています!)

gorillavim.connpass.com

Vimの折畳に対応したtreeコマンド

golang.tokyo #21DevQuizはGoでtreeコマンドを作成するというものでした。

treeコマンドといえば、以前Vimでファイル一覧をツリー表示するためのtree.vimというプラグインを作る時に使いました。

github.com

このプラグインディレクトリにマーカーをつけることでサブディレクトリを階層ごとに折り畳んで表示します。
当時、マーカーを(Go製の)自作コマンドでつけることも検討しましたが、実装が非常に面倒くさそうだったのでVim scriptでtreeコマンドの結果をパースすることにしました。

そんな事情があったため、せっかくなのでDevQuizをやりつつ、追加機能としてVimの折畳に対応した出力形式も実装してしまおう!と思って作ったのがgo-treeです。*1

こちらはコマンドラインフラグに-Vをつけると以下のような出力になります。

$ go-tree -V github.com/daisuzu/tree.vim
github.com/daisuzu/tree.vim/../
github.com/daisuzu/tree.vim/./
github.com/daisuzu/tree.vim/README.md
github.com/daisuzu/tree.vim/autoload/{{{
  github.com/daisuzu/tree.vim/autoload/tree.vim}}}
github.com/daisuzu/tree.vim/ftplugin/{{{
  github.com/daisuzu/tree.vim/ftplugin/tree.vim}}}
github.com/daisuzu/tree.vim/plugin/{{{
  github.com/daisuzu/tree.vim/plugin/tree.vim}}}
github.com/daisuzu/tree.vim/syntax/{{{
  github.com/daisuzu/tree.vim/syntax/tree.vim}}}

コマンド自体はVimから実行しても良いし、

:r! go-tree -V [DIR]

シェルで実行した結果をVimに流し込んでもOKです。

# -cでfoldmethod(fdm)をmarkerにする
go-tree -V [DIR] | vim - -R -c 'setl fdm=marker'

# モードラインでfoldmethod(fdm)をmarkerにする
echo -e "vim: fdm=marker\n$(go-tree -V [DIR])" | vim - -R

そうするとgithub.com/daisuzu/tree.vimVimで以下のように表示されます。

github.com/daisuzu/tree.vim/../
github.com/daisuzu/tree.vim/./
github.com/daisuzu/tree.vim/README.md
+--  2 lines: github.com/daisuzu/tree.vim/autoload/-----------------------------
+--  2 lines: github.com/daisuzu/tree.vim/ftplugin/-----------------------------
+--  2 lines: github.com/daisuzu/tree.vim/plugin/-------------------------------
+--  2 lines: github.com/daisuzu/tree.vim/syntax/-------------------------------
~
~
~
~

さらに、コマンド単体で動作するのでjob機能を使った非同期実行も簡単です。

:vnew | setl fdm=marker | call job_start('go-tree -V', {'out_io': 'buffer', 'out_buf': bufnr('%')})

f:id:daisuzu:20190128202818g:plain

このように、github.com配下に大量のファイルがあるような場合でもVimをブロックすることなく操作を継続することができます。

現在はvimrcにAtreeコマンドを定義してgo-treeを常用しています。
filetypeをtree.vimのものにすることで、そのまま表示するよりも少し見栄えが良くなっています。

command! -nargs=? -complete=dir -count -bang -bar ATree
      \ call s:async_tree(<q-args>, <count>, <bang>0, <q-mods>)

function! s:async_tree(dir, depth, bang, mods) abort
  let cmd = 'go-tree -V ' . g:tree_options
  if a:bang
    " コマンドに!をつけたら隠しファイルも表示する
    let cmd .= ' -a'
  endif
  if a:depth > 0
    let cmd .= ' -L ' . a:depth
  endif
  " cd後もgfできるようにディレクトリ名を絶対パスにする
  let cmd .= ' ' . fnamemodify(a:dir != '' ? a:dir : '.', ':p:h')

  execute a:mods . ' new'
  setfiletype tree

  let bufnr = bufnr('%')
  call job_start(cmd, {
        \   'out_io': 'buffer',
        \   'out_buf': bufnr,
        \   'exit_cb': {channel, msg -> s:goto_first(bufnr)},
        \ })
endfunction

" オリジナル版はカーソル位置が先頭行になるので同じ挙動にする
function! s:goto_first(bufnr)
  if bufnr('%') != a:bufnr
    " バッファを移動していたら何もしない
    return
  endif
  if line('.') != line('$')
    " 行を移動していたら何もしない
    return
  endif
  normal gg
endfunction

予想通り実装は面倒でしたが、Vimをより快適に使えるようになりました!

*1:人が見ることはあまり考えてませんでした...

Vimのexecute()と組み合わせて便利なコマンド

この記事はVim Advent Calendar 2018の13日目の記事です。

先月に開催されたVimConf 2018で、Migrating plugins to standard featuresというタイトルで発表してコマンドの結果をバッファに表示する方法を紹介しました。
その中で解説しきれなかったテクニックとして、以下のようなコマンドを定義しておくとL <コマンド>*1のようにして簡単に任意のコマンドの結果をバッファに表示することが出来るようになります。

command! -bar ToScratch
      \ setlocal buftype=nofile bufhidden=hide noswapfile

command! -nargs=1 -complete=command L
      \ <mods> new | ToScratch |
      \ call setline(1, split(execute(<q-args>), '\n'))

当日はプラグイン一覧やMRUを表示する用途として、

  • :scriptnames
  • v:oldfiles(:oldfilesでも良い)

を紹介しましたが、簡単に使えるなら他にも便利なコマンドがあるんじゃないかと思って調べてみました。
なお、対象は:exusageから

  • 引数不要
  • ファイルタイプやカーソル位置、バッファの状態などに依存しない
  • 結果が複数行になる

コマンドのみとしています。(収拾ががつかなくなりそうなので...)

定義系

各種定義を一覧表示するのは何かと便利そうです。
年末にvimrcを掃除する際などにも大活躍するんじゃないでしょうか?

  • :augroup ... 自動コマンドのグループ
  • :autocmd ... 自動コマンド
  • :abbreviate ... 短縮入力
    • :noreabbrevや、先頭にc, iを付けても良い
  • :command ... コマンド
  • :function ... 関数
  • :highlight ... ハイライト
    • :runtime syntax/hitest.vimだと実際にハイライトされる
  • :let ... 変数
  • :map ... マップ
    • :noremapや、先頭にc, i, l, n, o, s, t, v, xを付けても良い
  • :menu ... メニュー
    • :noremenuや、先頭にa, c, i, n, o, s, tl, v, xを付けても良い

開いているもの

gfで開く用途には向きませんが、どちらも大量に開いている時はバッファに表示されると便利そうです。

  • :buffers ... バッファ
    • または:files, :ls
  • :tabs ... タブ

各種操作結果

こちらも大量に表示されるような状況では便利そうです。

  • :registers ... レジスタ
    • または:display
  • :marks ... マーク
  • :messages ... メッセージ
  • :history ... コマンド履歴

Vimの情報

Vim関連の様々な情報はバッファに表示して見たいというケースはあまりないかもしれません。
個人的には代替の手段の方が使い勝手が良いと思っています。

  • :compiler ... コンパイラ用設定
  • :digraphs ... ダイグラフ
    • :h digraph-tableの方が見やすいかもしれない
  • :set ... 既定値と異なるオプション
    • :setglobal:setlocalもある
    • :optionsでも良いかもしれない
  • :version ... バージョン番号やその他の情報

デバッグ向け

Vim script開発者向け。

QuickFix

切り替える際に表示するだけならわざわざバッファに表示する必要はないかもしれません。

  • :chistory ... grepやmakeなど、quickfixリストの履歴
    • locationリストの場合は:lhistory
  • :clist ... quickfixリストの内容
    • locationリストの場合は:llist

移動系

こちらも一時的に表示するだけならバッファに表示する必要はなさそうです。
※実際に移動できるマッピングなどがあれば...*2

  • :jumps ... ジャンプリスト
  • :tags ... タグスタック
  • :tselect ... タグ

その他

ということで最後はこちら。

  • :smile ... 「ついに見つけたんだね、セイバー!」
                            oooo$$$$$$$$$$$$oooo
                        oo$$$$$$$$$$$$$$$$$$$$$$$$o
                     oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o         o$   $$ o$
     o $ oo        o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o       $$ $$ $$o$
  oo $ $ "$      o$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$o       $$$o$$o$
  "$$$$$$o$     o$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$o    $$$$$$$$
    $$$$$$$    $$$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$$$$$$$$$$$$$$
    $$$$$$$$$$$$$$$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$$$$$$  """$$$
     "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$
      $$$   o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$o
     o$$"   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$       $$$o
     $$$    $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
    o$$$oooo$$$$$  $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$   o$$$$$$$$$$$$$$$$$
    $$$$$$$$"$$$$   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     $$$$""""""""
   """"       $$$$    "$$$$$$$$$$$$$$$$$$$$$$$$$$$$"      o$$$
              "$$$o     """$$$$$$$$$$$$$$$$$$"$$"         $$$
                $$$o          "$$""$$$$$$""""           o$$$
                 $$$$o                                o$$$"
                  "$$$$o      o$$$$$$o"$$$$o        o$$$$
                    "$$$$$oo     ""$$$$o$$$$$o   o$$$$""
                       ""$$$$$oooo  "$$$o$$$$$$$$$"""
                          ""$$$$$$$oo $$$$$$$$$$
                                  """"$$$$$$$$$$$
                                      $$$$$$$$$$$$
                                       $$$$$$$$$$"
                                        "$$$""""

*1:Listの略

*2:i.e. Unite

golang.tokyo 静的解析Dayでgolang.org/x/tools/go/analysisを使ってみた

golang.org/x/tools/go/analysis 面白そうだなー。 けど触る時間がなかなか取れないなー。 と思っていたらちょうどタイムリーなツイートがあり、

数時間後にこちらの開催が決まりました。

golangtokyo.connpass.com

ということでモクモクとgschonnef.co/go/tools/lintからgolang.org/x/tools/go/analysisへの置き換えをやってきました。

ちなみにgscgolang.tokyo #14のLT*1で紹介した自作のlinterです。

変更点をざっくりまとめると、

  • honnef.co/go/tools/lintではCheckerというinterfaceに実装をするが、golang.org/x/tools/go/analysisではAnalyzerというstructになる
  • honnef.co/go/tools/lintではJobを受け取ってast.Inspectで処理をしていくが、golang.org/x/tools/go/analysisではPassを受け取ってInspector.Preorderで処理をしていく
    • AnalyzerRequiresフィールドに必要な前処理を指定する
    • 次の処理に結果を渡すような作りにすることも可能
  • コマンド本体はlintutil. ProcessFlagSetからmultichecker.Mainになる
    • ただしmainでflagを扱えなくなってしまう!
      • ツール全体でExitCodeを0にするといった制御が不可に...
      • なのでPassReportフィールド(関数)を書き換えることで 無理矢理 実現した
  • テストはhonnef.co/go/tools/lint/testutilgolang.org/x/tools/go/analysis/analysistestになる
    • 使い方はだいたい同じ

といったところです。

統一的なインターフェースになって再利用しやすくなるのもメリットですが、処理速度も向上していて、 手元で3秒くらいかかっていたのが一瞬で終わるようになりました。

今日はまだgolang.org/x/tools/go/analysisを使いこなすところまではいけませんでしたが、とても有意義な時間を過ごせました。

便利なAnalyzerが増えるとみんな幸せになれるのでもっともっと使う人が増えていくと良いですね!

VimConf 2018に行ってきた

VimConf 2018に行ってきました。

今年は@mattn_jpさんだけじゃなく、Vim作者のBramさんも発表するという超豪華Keynoteでした。
また、今年は去年以上に国際色高まるカンファレンスだったんじゃないかなと思います。
運営のみなさま、とても大変だったと思いますがすごく楽しい1日を過ごすことができました。
本当にありがとうございます。

以下セッションの一言感想です。

What is the next feature? by Yasuhiro Matsumoto

vim-jpについてのエモい話。
自分もVimにパッチを送る時、vim-jpにはとてもお世話になりました。
それにしても(ある程度形になった)パッチを用意してプレゼンに臨むのは流石です。

Vim: From hjkl to a platform for plugins by Bram Moolenaar

Vimの新機能について作者本人から話が聴けるというすごく貴重な発表でした。
プラグイン管理やVimの表現力がさらに充実していきそうで今後がとても楽しみです。
もちろんVim scriptのパフォーマンス向上も!

Migrating plugins to standard features by daisuzu

自分の発表。
Shougo wareを標準機能で置き換える話をしてきました。

Modes by Tatsuhiro Ujihisa

:Termdebugを使ったモードの実装の解説。
前にMeguro.vim:Termdebugを使っているところを見せてもらいましたが、とても便利そうでした。

A day in the life of (ordinary) Vimmer by OKURA Masafumi

Ordinary Vimmerは毎朝アップデートしますよね。
Learn once, use anywhere.
自分の発表で言いたかったことはこれです。

Modern editor-independent development environment for PHP by USAMI Kenta

普段はEmacsを使っていらっしゃる。
自分でHackできるツールが良いというのは完全に同意です。

Effective Modern Vim scripting by Alisue

Create your own plugin!
すごくわかります。
vital-Whiskyの中にあるRxがすごく気になりました。

Oni - The GUI-fication of Neovim by Akin

Electron(React + TypeScript)製のneovimクライアントの紹介。
バッファを分割してブラウザを開けるのはとても便利そうでした。

Vim ported to WebAssembly by rhysd

今日1番濃い発表だったんじゃないか...?
Vimの内部処理の解説がとても詳しくてすごく勉強になりました。
それにしても8日でやってしまうのが本当にすごい。

grpc-gatewayでProtocol Buffers over HTTP

grpc-gatewayを使うとgRPCサーバをRESTfulなインターフェースで叩けるようになります。
APIクライアントはswaggerから生成しても良いのですが、Goだとprotocで生成したstructをPOSTする方が依存も少なく、楽なこともあるでしょう。
ということで軽く試してみました。

Protocol Buffersのシリアライズ/デシリアライズ

まず、サーバがProtocol Buffersをやりとりできるようにしてあげないといけません。
そのための機能は全てgrpc-gatewayruntimeパッケージにあるので、以下のようにしてServeMuxを初期化すればOKです。

mux := runtime.NewServeMux(
    runtime.WithMarshalerOption("application/octet-stream", new(runtime.ProtoMarshaller)),
)

APIクライアント

これでクライアントからapplication/octet-streamでProtocol Buffersを投げることができるようになりました。

func do(url string, in *pb.ExampleRequest) (*pb.ExampleResponse, error) {
    body := new(bytes.Buffer)
    if err := new(runtime.ProtoMarshaller).NewEncoder(body).Encode(in); err != nil {
        return nil, err
    }

    res, err := http.Post(url, "application/octet-stream", body)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    if res.StatusCode >= 400 {
        b, err := ioutil.ReadAll(res.Body)
        if err != nil {
            return nil, errors.New(res.Status)
        }
        return nil, fmt.Errorf("%s: %s", res.Status, b)
    }

    out := new(pb.ExampleResponse)
    if err := new(runtime.ProtoMarshaller).NewDecoder(res.Body).Decode(out); err != nil {
        return nil, err
    }
    return out, err
}

エラーの型を共通化する

このままでも普通に使う分には問題ありませんが、サーバがエラーレスポンスを返した際もBodyをstructにデコードできるとより嬉しいです。
現状だと、サーバ側のエラーレスポンスはデフォルトで以下のunexportedな型になっています。

type errorBody struct {
    Error   string     `protobuf:"bytes,1,name=error" json:"error"`
    // This is to make the error more compatible with users that expect errors to be Status objects:
    // https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto
    // It should be the exact same message as the Error field.
    Message string     `protobuf:"bytes,1,name=message" json:"message"`
    Code    int32      `protobuf:"varint,2,name=code" json:"code"`
    Details []*any.Any `protobuf:"bytes,3,rep,name=details" json:"details,omitempty"`
}

// Make this also conform to proto.Message for builtin JSONPb Marshaler
func (e *errorBody) Reset()         { *e = errorBody{} }
func (e *errorBody) String() string { return proto.CompactTextString(e) }
func (*errorBody) ProtoMessage()    {}

そのため、クライアント側でデコードするためにはこちらをコピーして使うか、サーバ側で型を変更する必要があります。

エラーの型を変える場合はWithProtoErrorHandlerを使います。

mux := runtime.NewServeMux(
    runtime.WithMarshalerOption("application/octet-stream", new(runtime.ProtoMarshaller)),
    runtime.WithProtoErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
        w.Header().Set("Content-Type", marshaler.ContentType())

        s, ok := status.FromError(err)
        if !ok {
            s = status.New(codes.Unknown, err.Error())
        }

        buf, merr := marshaler.Marshal(s.Proto())
        if merr != nil {
            w.WriteHeader(http.StatusInternalServerError)
            io.WriteString(w, `{"error": "failed to marshal error message"}`)
            return
        }

        w.WriteHeader(runtime.HTTPStatusFromCode(s.Code()))
        w.Write(buf)
    }),
)

内容はDefaultHTTPErrorとほとんど同じですが、エラーの型をgoogle.golang.org/genproto/googleapis/rpc/status(*spb.Status)にしています。

こうすることで、クライアント側はgoogle.golang.org/genproto/googleapis/rpc/statusspb*1としてimportし、proto.Unmarshal*spb.Statusに戻せるようになります。

if res.StatusCode >= 400 {
    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, errors.New(res.Status)
    }

    v := new(spb.Status)
    if err := proto.Unmarshal(b, v); err != nil {
        return nil, fmt.Errorf("%s: %s", res.Status, b)
    }
    return nil, status.ErrorProto(v)
}

返ってきたエラーはstatus.FromError*status.Statusにすると、CodeMessageDetailsが取り出せるので後続の処理で自由に使えます。

if s, ok := status.FromError(err); ok {
    log.Printf("code: %d, message: %s, details: %v", s.Code(), s.Message(), s.Proto().Details)
}

なお、Detailsは型情報が落ちてしまっているため、内容を正しく出力するにはptypes.UnmarshalAnyで元に戻してあげる必要があります。

例えばDetailserrdetails.BadRequestの場合だと以下のようなコードです。

if s, ok := status.FromError(err); ok {
    var details []string
    for _, d := range s.Proto().Details {
        var m errdetails.BadRequest
        if err := ptypes.UnmarshalAny(d, &m); err == nil {
            details = append(details, m.String())
        }
    }
    log.Printf("code: %d, message: %s, details: %v", s.Code(), s.Message(), details)
}

s.Details()だと[field_violations:<field:"data" description:"invalud" > ]
s.Proto().Detailsだと[type_url:"type.googleapis.com/google.rpc.BadRequest" value:"\n\017\n\004data\022\007invalud" ]になってしまうのが、
ちゃんと[field_violations:<field:"data" description:"invalud" > ]になります。

*1:statusだと"google.golang.org/grpc/status"とかぶるため