methaneのブログ

このブログに乗せているサンプルコードはすべてNYSLです。

Sony の SBH90C レビュー

https://www.amazon.co.jp/dp/B07CZQMYKK/ref=as_li_ss_tl?&hvadid=274798673996&hvpos=1o3&hvnetw=g&hvrand=13734372561970267267&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1028853&hvtargid=pla-465703230864&th=1&psc=1&linkCode=ll1&tag=py0d-22&linkId=9446964464023e4d4ba0cec75ac27735&language=ja_JP

それまでは Anker の Zolo Liberty を使っていたけれども、遅延が激しくてアニメ鑑賞でも気になっていたので買い替えた。 Zolo Liberty は codec が AAC なうえに、完全ワイヤレスだから左右間の再転送も必要で遅くなっているんだと思う。2019年のTWS Plus と apt-x Adaptive に期待している。

SBH90C は apt-x に対応しているのと、ネックバンド型だから左右転送の遅延もなく、きっと無線も安定しているだろうというのでチョイス。 Type-C ケーブルでデジタル接続に対応しているのもオタク心を刺激する。

感想&気になった点。

  • 専用のType-C ケーブルの太さはまだ良いが、しなやかさがイヤホンケーブルとしてはちょっと。。。ケーブルを柔らかくできないなら有線はアナログのほうが良かったと思う。
  • 有線時の遅延はスクフェスプレイに問題が無いレベル。
  • イヤホンジャック利用時はスマホの横画面でどちらを上にしたほうがスクフェスプレイがやりやすいかがデフォルトの横画面の向きと逆で、「回転」をOn/Offする必要があった。 Type-C ジャックは中央にあるので画面表示に合わせてスマホを持てて楽。
  • 無線の安定性は驚くほど上がった。中目黒駅での乗り換えで Liberty は壊滅状態だったが、 SBH90C は無問題。
  • 無線の遅延も動画視聴してる限りは気にならない。(音ゲーは最初から期待してないので試してないが、それ以外のゲームなら多分問題ない)
  • カナル型なのに、遮音性が驚くほど低い。「ポート」と呼ばれる穴があるせい。ノイズキャンセリングも無いので、通勤用途には厳しかった。セロテープかなにかでポートを塞いで見るつもり。
  • torne mobile が USB 接続だとエラーになる!スクフェス後にそのままアニメ見たいときや、充電したいときに困る。大人の事情だとは思うけどこういうつまらない制限でユーザビリティを下げるのはやめてほしい。

結論: 通勤用途には WI-1000X (と Type-C → アナログ変換ケーブル) のほうが向いてそう。

MySQL Connector/C の現状

MySQL 8 が GA になってたときは MySQL がどういうつもりなのかいまいち分からなかったのですが、 8.0.13 リリースを機に改めて調べてみたらこんな感じでした。

  • 従来の MySQL Connector/C は単体提供されない。サーバーをインストールしたらついてくる。
  • MySQL Connector/C++ は、client/server protocol の方のAPIC++ しか対応していない。 X Dev API の方は C 言語でも利用可能な形式になっている。

MySQL :: Download Connector/C (libmysqlclient)

ところで、Windows 版クライアントを考えたときに面白いのが MariaDB Connector/C です。 MySQL Connecotr/C が Windows 版でも OpenSSL 同梱になった等、 static link でシングルバイナリを作る使い方がほぼ不可能になったのに対して、 MariaDB Connector/C は OpenSSL を使わずに WindowsAPI を使って sha256_password や SSL 接続に対応していて、開発版では caching_sha2_password にも対応しました。

MySQL Connector/C がどうなってるのか公式のアナウンスがなかったのと、 MySQL Connector/Python が site-packages 直下に OpenSSL とか MySQL client の DLL とかをブッ込んでくる強硬手段を取ってきたので同じことをするとコンフリクトするので、 mysqlclient-pythonWindows版の wheel の提供を中断していました。

MariaDB Connector/C であれば static link で DLL ブッコミ不要の従来どおりの提供ができそうなので、 caching_sha2_password 対応版が出たらそっちに切り替えるつもりでいます。

Python の repr(float) を高速化する

Python の repr(float) は、 float(repr(float)) が保証される最短の表現を返します。 実装には netlib の dtoa を使っています。

この dtoa より速い実装が世の中にはあって、 V8 なんかで使われているようです。

github.com

float の repr の実装をこの速いやつに入れ替えるパッケージが frepr です。 frepr.install()float.__repr__ を入れ替えてしまうので、 jsonエンコードなども速くすることができるらしい。

Python で float のデータを大量に扱う人には嬉しそう。

ISUCON 8 予選の Go 初期実装に見る初心者コード

会社のBlogにも書いたのですが、ISUCON 8 予選で負けてきました。

さて、 ISUCON の初期実装の定番として、初心者が書いたようなSQLやコードになっている点が挙げられます。

今回の Go の初期実装もその定番にもれず、初心者がやりがちな、Goの良さを殺してしまうコードがありました。

今回負けた反省点の一つとして、アプリの書き換えを二人でやっていたのでコンフリクトを恐れてそのリファクタリングを怠ったというのもあります。 Goで本戦に参加されるチームの方にはぜひこれを克服してもらいたいと思います。

アンチパターン: 長い無名関数

例: https://github.com/isucon/isucon8-qualify/blob/9d7890f5433bdaf2cec75b4cdf1ebd0d9a531281/webapp/go/src/torb/app.go#L404-L492

   e.GET("/api/users/:id", func(c echo.Context) error {
        var user User
        if err := db.QueryRow("SELECT id, nickname FROM users WHERE id = ?", c.Param("id")).Scan(&user.ID, &user.Nickname); err != nil {
            return err
        }

        loginUser, err := getLoginUser(c)
        if err != nil {
            return err
        }
        if user.ID != loginUser.ID {
            return resError(c, "forbidden", 403)
        }
... [以下数十行]

Sinatra 風のコードを移植しようとするとありがちなのですが、関数名が main.func8 とかになってしまって、Goの良さであるスタックトレースやプロファイルの使い勝手を大きく損ねます。Goでこういう無名関数の使い方はやめましょう。 (RubyJavaScript では良いのかという話もありますが、それはまた文化が違うので…)

例えば sort.Slice() にわたす比較関数のようにごく短く、中からDBアクセスなどの処理をしない関数は無名関数にしてもいいです。

また、1つの通常関数の中に無名関数が1つかせいぜい2つあるくらいなら、やはり害は少ないです。

スタックトレースとか flamegraph に main.func6 とか main.func8 とかがバラバラでるのはダメです。

リファクタリング: 通常関数への書き換え

無名関数全体を移動し、適当に名前をつけましょう。

ISUCON であれば事前練習で、なにか機械的命名規則を決めてしまうと命名に時間を取られずに済みます。 例えば上の "/api/users/:id" を扱う関数であれば、 handle_GetApiUsersId とかで良いです。Goの慣習の命名規則(アンダースコア使わない、 API とか ID とかは Api, Id にしない)からは大きくハズレますが、 ISUCON では悩まず機械的に作業できる利点の方が大きいです。

アプリの書き換えを複数人でやる場合、このリファクタリングは書き換え範囲が大きくてコンフリクトが厳しいので、真っ先にえいっとやってしまうと良いと思います。

アンチパターン: for rows.Next() ループ内でのクエリ実行

例: https://github.com/isucon/isucon8-qualify/blob/9d7890f5433bdaf2cec75b4cdf1ebd0d9a531281/webapp/go/src/torb/app.go#L236-L252

   rows, err := db.Query("SELECT * FROM sheets ORDER BY `rank`, num")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    for rows.Next() {
...
        err := db.QueryRow("SELECT * FROM reservations WHERE event_id = ? AND sheet_id = ? AND canceled_at IS NULL GROUP BY event_id, sheet_id HAVING reserved_at = MIN(reserved_at)", event.ID, sheet.ID).Scan(&reservation.ID, &reservation.EventID, &reservation.SheetID, &reservation.UserID, &reservation.ReservedAt, &reservation.CanceledAt)

こんな感じで、 rows.Next() ループの中でクエリを実行したり、クエリの実行を含む関数を呼び出すと、複数のDBのコネクションを利用してしまいます。

そしてもっと悪いことに、他の prefork 型のアプリではプロセス数を適当に絞ることができるのに対して、GoでDBのコネクションプールを絞ろうとすると上のようなコードでデッドロックの原因になります。デッドロックを避けるためにコネクションプールの上限を設定しないと、たとえCPUが2コアしかないようなMySQLサーバーに対して数百コネクションから並列でクエリを投げてしまい、遅いクエリがどれか解らないとか、MySQL側がデッドロックを誤検出するとか、いろんなトラブルの原因になります。

for rows.Next() ループの中ではそのクエリの結果のフェッチだけを行い、その結果の各行に対する処理は改めて別のループに書きましょう。

アンチパターン: 長い関数での defer rows.Close()

https://github.com/isucon/isucon8-qualify/blob/9d7890f5433bdaf2cec75b4cdf1ebd0d9a531281/webapp/go/src/torb/app.go#L404-L492

上のアンチパターンの亜種ですが、せっかく for rows.Next() ループ内からDBにアクセスする処理を排除しても、 defer rows.Close() を使っていると、そのクエリに使われたコネクションは関数が終わるまでコネクションプールに返却されません。

とはいえ、安易に defer rows.Close()for rows.Next() ループの後ろに rows.Close() の形で移動するのもダメです。 for rows.Next() ループ内に return が無いか確認しましょう。

リファクタリング: sqlx の利用

上のような問題の「まっとうな」リファクタリング方法は、1つのクエリを実行して結果をフェッチするまでを個別に関数に切り出すことです。そうすると defer rows.Close() が適切なタイミングで実行されます。

しかし ISUCON だといちいちそういったリファクタリングをしている余裕がないかもしれません。そこで sqlx を覚えておくと良いでしょう。

sqlx は、 database/sql の上位互換になっています。 sql.Opensqlx.Open に書き換えるだけで、そのままのコードが動きます。

そして、 db.QueryRow(...).Scan(&data)db.Get(&data, ...) に、 db.Query(...), for rows.Next() { var row Record; rows.Scan(&row); records = append(records, row) } のようなパターンを db.Select(&records, ...) に書き換える事ができます。

特に後者が強力で、 rows.Next()rows.Close() を排除することができるので、コネクションを無駄に大量消費する問題を楽に解決することができます。

それ以外にも便利機能がいくつかあるので、 database/sql を直接使った初心者コードをリファクタリングするときの強力な武器として練習しておくことをおすすめします。

hub コマンドの BDD がユーザードキュメントとして素晴らしい

OSSメンテナをしていると他人のPRやブランチをチェックアウトして何かを確認したいということは頻繁にあって、いちいち git remote add して fetch してってのが面倒なので Github 製の Github CLI クライアントである hub を愛用している。

でも hub コマンドって、ドキュメントがあまりなくて、 help コマンドの出力も最小限で、頻繁に使う一部の機能以外はほとんど使いこなせずにいた。

しかし、 hub コマンドが Cucumber を使って BDD をしているのを最近知った。「どういう仕組でBDDが動いているのか」は全くわからないけれども、 「hub がどういうコマンドを実行するとどういう動作をするのか」は凄くわかりやすい。

たとえば、 hub pr checkout コマンドの Behavior を見てみると、

Feature: hub pr checkout <PULLREQ-NUMBER>
  Background:
    Given I am in "git://github.com/mojombo/jekyll.git" git repo
    And I am "mojombo" on github.com with OAuth token "OTOKEN"


  Scenario: Checkout a pull request
    Given the GitHub API server:
      """
      get('/repos/mojombo/jekyll/pulls/77') {
        json :number => 77, :head => {
          :ref => "fixes",
          :repo => {
            :owner => { :login => "mislav" },
            :name => "jekyll",
            :private => false
          }
        }, :base => {
          :repo => {
            :name => 'jekyll',
            :html_url => 'https://github.com/mojombo/jekyll',
            :owner => { :login => "mojombo" },
          }
        },
        :maintainer_can_modify => false,
        :html_url => 'https://github.com/mojombo/jekyll/pull/77'
      }
      """
    When I run `hub pr checkout 77`
    Then "git fetch origin refs/pull/77/head:fixes" should be run
    And "git checkout fixes" should be run
    And "fixes" should merge "refs/pull/77/head" from remote "origin"
  1. mojombo/jekyll をチェックアウトしたリポジトリにいるときに
  2. hub pr checkout 77 を実行すると、
  3. hubが git fetch origin refs/pull/77/head:fixes を実行して、 (ここで "fixes" は上の Github API のレスポンスで判断していることがなんとなくわかる)
  4. git checkout fixes を実行してくれる

事がわかる。

例となるシナリオを用意して、どんな git コマンドを実行してくれるのかわかる。しらない git コマンドがあればそれは git のマニュアルで調べればいい。

コマンドごとにいくつかシナリオが用意されているので、 hub help コマンド名 するよりも、この feature ファイルを探して斜め読みするほうがずっと hub コマンドで何ができるのか具体的に理解できる。