2017年04月18日

大幅にパワーアップした「ESP32」で mruby を動かす

はてなブックマークに登録

安価な Wi-Fi 対応モジュールとして人気を博した「ESP8266」の後継機種「ESP32」が各所で話題になっていますね。僕も少し前に秋月にパーツを仕入れに行った際に開発ボードが山積みされているのを見かけて買って(積んで)いました。このところプリント基板ばかり作っていたのですが、最近になってようやく弄りはじめたので記事を書きます。

ESP32−DevKitC

ESP8266 と ESP32 の比較

ざっくりですが ESP8266 と ESP32 のスペックを比べてみます。

(念のため、ESP8266 と ESP32 は、それぞれ SoC 単体の名称です。SoC 単体では容易に利用できないため、外部接続のフラッシュメモリやアンテナなど必要な部品をまとめてモジュール化した製品が複数のベンダーから提供されています。ここでは、最も広く流通している Espressif 謹製の ESP-WROOM シリーズにて比較しています)

ESP-WROOM-02 (ESP8266EX)ESP-WROOM-32 (ESP32-D0WDQ6)
ProcessorTensilica Xtensa L106 (32bit Single-Core 80/160 MHz)Tensilica Xtensa LX6 (32bit Dual-Core 160/240 MHz)
SRAM160KB(available to user < 50KB)520KB
Flash4MB4MB
Wi-Fi802.11 b/g/n (2.4GHz)802.11 b/g/n (2.4GHz)
Bluetooth-4.2 BR/EDR + BLE

ESP-WROOM-32 では、プロセッサがシングルコアの Xtensa L106 からデュアルコアの LX6 に強化され、SRAM の容量が大幅に拡大されています。新たに Bluetooth も搭載されています。

データシートを細かく見てみると、ESP-WROOM-32 では利用可能なペリフェラルの多さにも目を惹かれます。

こういったリソースの限られた環境ではプロセッサの性能よりもメモリの制限に悩まされることが多いので、SRAM の容量が格段に上がっているのは大きなポイントです。

このブログでも以前に ESP8266 に関する記事(ESP8266 モジュールの AT コマンドに SSL クライアント機能を追加する)を載せていますが、その際に「メモリが足りなくて SSL の処理が正常に行われない」という問題に遭遇しています。このケースでは、最終的には SDK 側で「メモリを 12KB 節約する」という修正が加わって解決しましたが、たかだか十数 KB のメモリをやりくりしなければならない世界なのです。

そんなわけで、520KB のメモリを搭載しているのは「ん?富豪かな?」というくらいの衝撃です。そして、このくらいの水準になってくると「NetBSD 動かないかな」とか「Mono 動かないかな(*同僚が実践済み)」とか妄想し始めるわけです。

ちょうど個人的に mruby に興味を持ちはじめていたこともあり「mruby のフットプリントってどのくらいだっけ?」と思って調べてみたら 400KB くらいという情報が出てきてたので、ワンチャンありそうという感じでやってみましたというお話です。

開発環境のセットアップ

ESP32 の公式な開発環境は「ESP-IDF」と「Arduino-ESP32」の二種類あります。

前者は ESP32 の動作に必要なブートローダや FreeRTOS、各種コンポーネント一式をまとめたスタンダードなビルドシステムで、今回はこちらを使用します。ちなみに、後者は ESP32 上で Arduino 互換の API を実装したもので、Arduino IDE を使って開発する仕組みになっているようです。

toolchain と ESP-IDF のインストール

ドキュメントがしっかりと整備されているので、これを参照すれば困ることはあまりなさそうです。

一応、ここでは Mac OS X での手順を載せておきます。

フラッシュメモリへのデータ転送やシリアルモニタで pyserial が必要になるので予めインストールしておきます。

$ sudo easy_install pip
$ sudo pip install pyserial

クロスコンパイル用の toolchain(コンパイラやリンカなど諸々一式)のバイナリが用意されているので、ダウンロードして展開します。展開後はパスを通しておくだけで OK です。

$ mkdir ~/esp
$ cd ~/esp
$ curl -O https://dl.espressif.com/dl/xtensa-esp32-elf-osx-1.22.0-61-gab8375a-5.2.0.tar.gz
$ tar -zxvf xtensa-esp32-elf-osx-1.22.0-61-gab8375a-5.2.0.tar.gz
$ echo 'export PATH=$PATH:$HOME/esp/xtensa-esp32-elf/bin' >> ~/.profile
$ source ~/.profile

続いて、GitHub から ESP-IDF のプロジェクトをクローンします。submodule を使っているので --recursive を忘れずに指定してください。環境変数 IDF_PATH を設定したらインストール完了です。

$ cd ~/esp
$ git clone --recursive https://github.com/espressif/esp-idf.git
$ echo 'export IDF_PATH=$HOME/esp/esp-idf' >> ~/.profile
$ source ~/.profile

サンプルプログラムで動作確認

アプリケーション作成時にテンプレートとして使えるプロジェクトが用意されているので、これを使って動作確認をします。

$ cd ~/esp
$ git clone https://github.com/espressif/esp-idf-template.git myapp
$ cd myapp

テンプレートのプロジェクトをクローンしてきたら make menuconfig を実行して設定メニューを立ち上げます。

$ make menuconfig

立ち上がったメニューから Serial flasher config > Default serial port と進み、使用するシリアルポート(DevKitC を使っている場合には /dev/cu.SLAB_USBtoUART になると思います)を入力して Save & Exit します。

シリアルポートの設定だけすれば OK なので、make でプロジェクト全体をビルドします。

$ make

ビルドに成功したら、make flash で ESP32 のフラッシュメモリに生成したバイナリを転送します。make monitor でシリアルモニタを接続できるので、同時に指定しておけば転送後にそのままモニタリングできます。

$ make flash monitor
esptool.py v2.0-beta2

...(snip)...

Hello world!
Restarting in 10 seconds...
Restarting in 9 seconds...
Restarting in 8 seconds...
Restarting in 7 seconds...
Restarting in 6 seconds...
Restarting in 5 seconds...
Restarting in 4 seconds...
Restarting in 3 seconds...
Restarting in 2 seconds...
Restarting in 1 seconds...
Restarting in 0 seconds...
Restarting now.

サンプルプログラムは Hello world! を出力した後にカウントダウンを始めて10秒後にリブートする、という動作を繰り返します。なお、シリアルモニタからは Ctrl+] で抜けられます。

困った時の対処法

あえて理由は述べませんが、フラッシュメモリの内容をリセットしたい時というのは往々にしてあるものです。そういった時には make erase_flash を叩くと幸せになれます。

$ make erase_flash

ちなみに make erase_flash した後、どういう状態になっているかシリアルモニタを接続して確認してみると、おもむろに Basic ROM が立ち上がってくるなど中々に趣があります。

$ make monitor

...(snip)...

> help
A very Basic ROM console. Available commands/functions:
LIST
NEW
RUN
NEXT
LET
IF
GOTO
GOSUB
RETURN
REM
FOR
INPUT
PRINT
PHEX
POKE
STOP
BYE
MEM
?
'
DELAY
END
RSEED
HELP
ABOUT
IOSET
IODIR
PEEK
ABS
RND
IOGET
USR
>

mruby を動かす

開発環境の準備が整ったので mruby を動かす作業に移ります。世の中は偉大な先人達で溢れていて、大抵のことは既に他の誰かがやっていたりします。そんな訳で Google 先生に聞いてみたところ、3秒で「mruby-esp32」というプロジェクトがあることを教えてくれました。

mruby-esp32

そのものズバリ、ESP32 で mruby を動かす PoC です。

細かな説明はさておき、まずは動かしてみましょう。

GitHub から mruby-esp32 のリポジトリをクローンします。内部で submodule として mruby を参照しているため、--recursive が必要です。

$ git clone --recursive https://github.com/carsonmcdonald/mruby-esp32.git
$ cd mruby-esp32

サンプルプロジェクトの時と同様に、自分の環境に合わせてシリアルポートの設定をします。

$ make menuconfig

以下のようにビルドしてフラッシュへ転送します。make に渡している MRUBY_EXAMPLE については後ほど説明します。

$ make MRUBY_EXAMPLE=system_mrb.rb
$ make MRUBY_EXAMPLE=system_mrb.rb flash monitor

...(snip)...

SDK Version: v2.0-rc1-401-gf9fba35-dirty
Memory free: 209.488K
Delaying 10 seconds

正常に動作していれば、シリアルモニタに SDK のバージョンや空きメモリの情報が出力されます。

ざっくりと追ってみる

以下は mruby-esp32 のディレクトリ構成です。

.
|-- LICENSE
|-- Makefile
|-- README.md
|-- components
|   `-- mruby_component
|       |-- component.mk
|       |-- esp32_build_config.rb
|       |-- mruby
|       |-- mruby-esp32-system
|       `-- mruby-esp32-wifi
|-- main
|   |-- component.mk
|   |-- examples
|   |   |-- simplest_mrb.rb
|   |   |-- system_mrb.rb
|   |   `-- wifi_example_mrb.rb
|   `-- mruby_main.c
`-- sdkconfig

ESP-IDF のアプリケーションのエントリーポイントである app_main()main/mruby_main.c の中にあります。

#include <stdio.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"

#include "mruby.h"
#include "mruby/irep.h"

#include "example_mrb.h"

void mruby_task(void *pvParameter)
{
  mrb_state *mrb = mrb_open();
  mrb_load_irep(mrb, example_mrb);
  mrb_close(mrb);

  // This task should never end, even if the
  // script ends.
  while (1) {
  }
}

void app_main()
{
  nvs_flash_init();
  xTaskCreate(&mruby_task, "mruby_task", 8192, NULL, 5, NULL);
}

xTaskCreate() は FreeRTOS のタスクを生成する関数です。なので、実質 mruby_task() の処理が全てです。

  • mrb_open()
  • mrb_load_irep()
  • mrb_close()

やっているのはたったこれだけ。mruby の実行環境を準備して mrb_load_irep() で mruby のバイトコードを流し込んでいるだけという実にシンプルな処理です。mrb_load_irep() に渡している example_mrb が mruby のバイトコードですが、これは mruby_main.c には定義されておらず別のところにあるようです。example_mrb.h といういかにもそれっぽいヘッダファイルをインクルードしていていますが、何処にも見当たりません。

いろいろ探ってみると、main/component.mk に記述がありました。

COMPONENT_EXTRA_CLEAN := example_mrb.h

mruby_main.o: example_mrb.h

example_mrb.h: $(COMPONENT_PATH)/examples/$(MRUBY_EXAMPLE)
	$(COMPONENT_PATH)/../components/mruby_component/mruby/bin/mrbc -B example_mrb -o $@ $^

.PHONY: example_mrb.h

どうやら example_mrb.h はビルド時に生成してたようです。mrbc は mruby のコンパイラです。明示的にオプションを指定しない場合は、mruby の VM(RiteVM)が解釈できるバイトコードを生成しますが、-B オプションを付けると C で扱いやついように配列に格納した状態のコードを吐いてくれます。また、make を実行する際にしていた MRUBY_EXAMPLE という環境変数もここで登場しています。main/examples 配下にあるスクリプトの中から、 MRUBY_EXAMPLE で指定されたスクリプトがコンパイルされて使われる仕組みになっています。

以下は、実際に main/examples/system_mrb.rbmrbc -B でコンパイルしたものです。

/* dumped in little endian order.
   use `mrbc -E` option for big endian CPU. */
#include <stdint.h>
extern const uint8_t example_mrb[];
const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
example_mrb[] = {
0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x33,0x8d,0x5c,0x00,0x00,0x01,0xc2,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x01,0x86,0x30,0x30,
0x30,0x30,0x00,0x00,0x01,0x7e,0x00,0x03,0x00,0x07,0x00,0x00,0x00,0x00,0x00,0x29,
0x91,0x00,0x80,0x01,0x13,0x00,0x80,0x01,0x20,0x80,0x80,0x01,0x01,0xc0,0x80,0x00,
0x06,0x00,0x80,0x01,0x3d,0x00,0x00,0x02,0x01,0x40,0x80,0x02,0x3e,0x40,0x01,0x02,
0xa0,0xc0,0x80,0x01,0x91,0x00,0x80,0x01,0x13,0x00,0x80,0x01,0x20,0x00,0x81,0x01,
0x83,0xf3,0x41,0x02,0xb1,0x40,0x81,0x01,0x01,0xc0,0x00,0x01,0x06,0x00,0x80,0x01,
0x3d,0x01,0x00,0x02,0x01,0x80,0x80,0x02,0x3e,0x40,0x01,0x02,0xbd,0x01,0x80,0x02,
0x3e,0x40,0x01,0x02,0xa0,0xc0,0x80,0x01,0x06,0x00,0x80,0x01,0x3d,0x02,0x00,0x02,
0xa0,0xc0,0x80,0x01,0x91,0x00,0x80,0x01,0x13,0x00,0x80,0x01,0x83,0x04,0x40,0x02,
0x83,0xf3,0xc1,0x02,0xb0,0xc0,0x01,0x02,0xa0,0x80,0x81,0x01,0x06,0x00,0x80,0x01,
0xbd,0x02,0x00,0x02,0xa0,0xc0,0x80,0x01,0x91,0x00,0x80,0x01,0x13,0x00,0x80,0x01,
0x83,0x04,0x40,0x02,0x02,0x03,0x80,0x02,0xb0,0xc0,0x01,0x02,0xa0,0x00,0x82,0x01,
0x4a,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x0d,0x53,0x44,0x4b,0x20,0x56,
0x65,0x72,0x73,0x69,0x6f,0x6e,0x3a,0x20,0x00,0x00,0x00,0x00,0x00,0x0d,0x4d,0x65,
0x6d,0x6f,0x72,0x79,0x20,0x66,0x72,0x65,0x65,0x3a,0x20,0x00,0x00,0x01,0x4b,0x00,
0x00,0x13,0x44,0x65,0x6c,0x61,0x79,0x69,0x6e,0x67,0x20,0x31,0x30,0x20,0x73,0x65,
0x63,0x6f,0x6e,0x64,0x73,0x00,0x00,0x1c,0x44,0x65,0x65,0x70,0x20,0x73,0x6c,0x65,
0x65,0x70,0x69,0x6e,0x67,0x20,0x66,0x6f,0x72,0x20,0x31,0x30,0x20,0x73,0x65,0x63,
0x6f,0x6e,0x64,0x73,0x01,0x00,0x07,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x00,0x00,
0x00,0x09,0x00,0x06,0x53,0x79,0x73,0x74,0x65,0x6d,0x00,0x00,0x05,0x45,0x53,0x50,
0x33,0x32,0x00,0x00,0x0b,0x73,0x64,0x6b,0x5f,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,
0x00,0x00,0x04,0x70,0x75,0x74,0x73,0x00,0x00,0x10,0x61,0x76,0x61,0x69,0x6c,0x61,
0x62,0x6c,0x65,0x5f,0x6d,0x65,0x6d,0x6f,0x72,0x79,0x00,0x00,0x01,0x2f,0x00,0x00,
0x05,0x64,0x65,0x6c,0x61,0x79,0x00,0x00,0x01,0x2a,0x00,0x00,0x0e,0x64,0x65,0x65,
0x70,0x5f,0x73,0x6c,0x65,0x65,0x70,0x5f,0x66,0x6f,0x72,0x00,0x4c,0x56,0x41,0x52,
0x00,0x00,0x00,0x1e,0x00,0x00,0x00,0x02,0x00,0x03,0x76,0x65,0x72,0x00,0x03,0x6d,
0x65,0x6d,0x00,0x00,0x00,0x01,0x00,0x01,0x00,0x02,0x45,0x4e,0x44,0x00,0x00,0x00,
0x00,0x08,
};

mruby_main.c がインクルードしていた example_mrb.h の実態はコレです。mrbc -B の結果をヘッダファイルとして保存し、それをインクルードすることで .rb ファイルをコンパイルした後のバイトコードが mruby_main.o のオブジェクトファイルにそのまま組み込まれます。

このように mruby-esp32 の中身をざっと追ってみて、以下のことが分かりました。

  • ESP-IDF のフレームワークを利用したアプリケーションとして mruby の VM を動作させている
  • ホスト側で事前にコンパイルした mruby のバイトコードをオブジェクトファイルに組み込んでいる
  • アプリケーション起動時にそのバイトコードを VM に渡して実行している

バイトコードをオブジェクトファイルに組み込んでしまっているため、.rb ファイルを変更するたびにアプリケーションの差し替え(再ビルド&フラッシュ書き込み)が発生してしまうのが少々手間ですが「ESP-IDF ではファイルシステムを扱うのになにかと手間がかかる」という事情を考えると、手軽に試すにはこれが最善の方法かもなと思いました。

また、mruby 本体については components/mruby_component 配下にありますが、これは git の submodule として参照しているだけで、コードには一切手が加えられていません。esp32_build_config.rb にクロスビルド用の設定が書かれているのみで、Xtensa + FreeRTOS という馴染みの薄い環境でサクッと動いてしまうのですから mruby の移植性の高さには驚かされます。

mruby REPL を動かす

ここで終わってしまうと、ただ「mruby-esp32 を動かしてみた」というだけの内容になってしまうため、もう少し踏み込んでみます。

先にも書きましたが、mruby-esp32 の仕組みだとデバイス上で動作しているのは mruby の VM だけです。VM に渡すバイトコードを生成するコンパイル作業はホスト側で行っていまるので、.rb ファイルを修正するたびにホストから ESP32 への転送が必要になり少し面倒です。そこで、デバイス上でコンパイルも含めて完結させられないかなと思いながら試してみました。

まず、mruby のコンパイラである mrbc をデバイス上で動作させようかと思ったのですが、ESP-IDF ではファイルシステムを一手間も二手間もかかりそうという問題があり、ちょっと面倒くさそうなので代替案として mirb でやってみることにしました。mirb は mruby の REPL(Read–eval–print loop)で、読んで字のごとく「読む」「評価する」「表示する」とう動作を繰り返す対話型のコマンドで、コンパイラとしての機能を含んでいます。

mirb - Embeddable Interactive Ruby Shell

> 1 + 1
 => 2

mruby-esp32-app-mirb

そんなこんなで、mirb を ESP32 で動作するようにしたものがコチラ。

mruby のクロスコンパイルの定義ファイルなど、だいぶ mruby-esp32 を参考にして作りました。

細かい話は後にして、実際に動作している様子がこちらです。

GitHub からプロジェクトをクローンしてきてビルドするだけで動かせるので是非試してみてください。

$ git clone --recursive https://github.com/pandax381/mruby-esp32-app-mirb.git
$ cd mruby-esp32-app-mirb
$ make menuconfig
$ make
$ make flash monitor

クロスビルド用の mruby 本体には標準の mgem を全て組み込んであります。カスタマイズしたい場合には components/mruby/build_config.rb を編集してください。

苦労話など

mirb のコードは mruby/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c で、1つのソースファイルに全ての処理が書かれています。とりあえず mruby-compiler の mgem を組み込めば動くんじゃないかという目論見のもと、雑に mirb.c の中身を丸ごとコピペし、リネームした main 関数を呼び出すようにしてみるところからスタートしました。

シグナルが未実装

まず、ジャブとしてビルド時に undefined reference to 'signal' と怒られます。ESP-IDF の足回りは FreeRTOS + newlib で構築されているのですが、シグナル関連の処理が実装されていないためリンクできずにエラーが発生しているようです。mirb のコードを見たところ、SIGINTCtrl+C で飛んでくるシグナル)を処理しているだけだったので、シグナル関連の処理をサクッとコメントアウトするとビルドが通るようになります。

STDIN がノンブロッキング

シグナル関連のケアをしてあげるとビルドが通って起動するようにるのですが、何故かバナーを出力して入力待ちになってすぐに再起動するという動作を繰り返してしまいます。何処でエラーになっているのか調べたところ、getchar()EOF を返して mirb の main 関数が終了してしまっていることが分かりました。getchar() が EOF を返すのは「ファイルの終わりに達した」か「エラーが発生した」場合です。getchar() は getc(stdin) と等価なので、入力対象は標準入力(stdin)であり、通常であればエラーが発生することは考えにくいです。そうなると「ファイルの終わりに達した = stdin が閉じられている = stdin が何処にもつながっていない」可能性がありそうと考え、ESP-IDF のドキュメントを確認したところ、以下のような記述がありました。

Standard IO streams (stdin, stdout, stderr)

(…snip...)

Writing to stdout or stderr will send characters to the UART transmit FIFO. Reading from stdin will retrieve characters from the UART receive FIFO.

Note that while writing to stdout or stderr will block until all characters are put into the FIFO, reading from stdin is non-blocking. The function which reads from UART will get all the characters present in the FIFO (if any), and return.I.e. doing fscanf("%d\n", &var); may not have desired results. This is a temporary limitation which will be removed once fcntl is added to the VFS interface.

どうやら、stdin は UART の RX とつながっているようです。しかし、ここに記述されているように STDIN はノンブロッキングに設定されています(そしてどうやら変更できない)。標準入出力の関数をノンブロッキングで扱うとか普通やらないんですけど、最終的な read システムコールあたりで EAGAINEWOULDBLOCK のエラーを返すので、getchar() がエラーとして EOF を返してしまっているようです。mirb は getchar() が EOF を返すと feof()ferror() の判定をせずに終了してしまっていますが、stdin がノンブロッキングになっていてその結果エラーが返っているとかエッジケースすぎて仕方ないよね...という感じです。この問題は、getchar() が EOF を返してもリトライするよう修正すればとりあえず動作するようになりますが、ビジーループでブロッキングすることになるので気が引けます。みんな困っていないのかな?と思って調べてみたところ、同じようにハマっている人達が見つかりました。どうやら uart_rx_one_char_block() という関数があるらしく、彼らはこれを使っているようです。関数名の通り、UART から 1 文字読むまでブロックしてくれたので、今回はこれで代用しました。

エスケープシーケンス

ようやく文字列の入力ができるようになったものの、シェルも何も介していないため stdin から読み込まれるデータはバッファリングされていません。キーを押した瞬間に getchar() が返り、カーソルキーを押すと ANSI エスケープされた数バイトのコードが飛んできます。エコー出力もされません。また、改行コードの変換もしてくれませんので、改行を \n だけで判定していると Mac の場合は永遠に改行に出会えません。まぁ、なんと言いますか readline の有り難みを再認識する瞬間ですね。

ちなみに、 mirb は、ビルド時に readline(または linenoise)を組み込めるようになっていますが、ESP-IDF には Terminfo がないようで一手間加えないと組み込めそうにありません。とりあえず、普段使っている getchar() の代わりになるようなものを自前実装で書いてみました。

#undef getchar
#define getchar uart_getchar

#define STATE_NML 0x00
#define STATE_ESC 0x01
#define STATE_CSI 0x02

static void
echo_rewrite(const uint8_t *s, size_t n, int cursor) {
  printf("\e[3G\e[J");
  printf("%.*s", n, s);
  if (n && s[n-1] != '\n') {
    printf("\e[%dG", 3 + cursor);
  }
  fflush(stdout);
}

static int
uart_getchar(void)
{
  static size_t n = 0;
  static uint8_t buf[1024];
  static uint8_t last = 0;
  uint8_t c, s = STATE_NML;
  int v = 0, cursor = 0;

  if (n) {
    c = buf[0];
    memmove(buf, buf + 1, --n);
    return (int)c;
  }
  do {
    c = uart_rx_one_char_block();
    if (last == '\r' && c == '\n') {
      continue;
    }
    last = c;
    if (c == '\b' || c == 0x7f) {
      if (n && cursor) {
        memmove(buf + (cursor - 1), buf + cursor, n - cursor);
        --n, --cursor;
        echo_rewrite(buf, n, cursor);
      }
      continue;
    }
    if (c == '\e') {
      s = STATE_ESC;
      continue;
    }
    if (c == '\r') {
      c = '\n';
    }
    switch (s) {
    case STATE_ESC:
      if (c == '[') {
        s = STATE_CSI;
        v = 0;
      } else {
        s = STATE_NML;
      }
      continue;
    case STATE_CSI:
      switch (c) {
      case '0'...'9':
        v = (v * 10) + (c - '0');
      case ';':
        v = 0;
      case 'A'...'z':
        switch (c) {
        case 'C':
          if (!v) {
            v = 1;
          }
          if (n - cursor < v) {
            v = n - cursor;
          }
          cursor += v;
          echo_rewrite(buf, n, cursor);
          break;
        case 'D':
          if (!v) {
            v = 1;
          }
          if (cursor < v) {
            v = cursor;
          }
          cursor -= v;
          echo_rewrite(buf, n, cursor);
          break;
        }
      default:
        s = STATE_NML;
      }
      continue;
    }
    if (c != '\n') {
      memmove(buf + (cursor + 1), buf + cursor, n - cursor);
      buf[cursor] = c;
      ++n, ++cursor;
    } else {
      buf[n++] = c;
      cursor = n;
    }
    echo_rewrite(buf, n, cursor);
  } while (c != '\n');
  return uart_getchar();
}

だいぶ雑に書いてあるのでツッコミどころは多々あると思いますが、以下の機能を実現しています。

  • 行バッファリング
  • カーソルキー(左右)での移動と挿入
  • BackSpace と Delete キー での消去
  • 改行コード変換
  • 入力のエコー出力

すごく地味ですが、mirb をちゃんと動かすために一番苦労した部分です。とりあえず「違和感なく入力できる」くらいにはなっていると思いますが、そのうち readline か linenoise 対応版を作ります。

おわりに

mirb は入力周りで一部修正を加えましたが、それ以外のコンパイラとしての主要部分は一切修正なく移植できました。VM もコンパイラもこんなカンタンに動いてしまって mruby すごいですね。というわけで、ESP32 で mruby が簡単に動くことが分かったので、みんな頑張って mgem を書いて充実させましょう!


@pandax381

pandax381 at 16:38
この記事のURLComments(0)TrackBack(0)
2017年04月17日

最近のPython-dev(2017-04)

はてなブックマークに登録

バックナンバー:

NEWS (changelog) の作り方

Mercurial時代からNEWSファイル (changelog) の扱いは面倒だったのですが、Githubに移行したことでよりコンフリクトが起こりやすくなり面倒さに拍車がかかりました。 また、コンフリクトせずに間違った状態でマージされるというかなり致命的な事故も起こってしまっています。 (ワークフローが cherry-pick になったためにマージ時に履歴が考慮されなくなったのか、それともMercurialよりもGitの方がマージがバカなのか、詳細は把握してません。)

それで、1つの大きなNEWSファイルにエントリを追記していく代わりに、1つのエントリだけを含む小さいファイルを追加していき、ツールでそれらのファイルからNEWSファイルを生成する仕組みへの移行が急務となり、ツールの選定のためにコンペが行われました。

Generate Misc/NEWS from individual files

コンペに参加したのは reno, towncrier, blurb の3つです。

reno は OpenStack で使われているツールで、エントリファイルのフォーマットがYamlだったりとちょっとクセがあります。

towncrier も既存のツールで、ファイルの拡張子やディレクトリ名で必要なメタデータ (機能追加かバグフィックスかパフォーマンス向上かなど) を表現するのでファイルの中身はただのテキストになります。また、新規エントリを追加するのにコマンドを使わずたんにファイルを追加するだけで済むのも魅力です。NEWSをビルドするときに towncrier を必要とするものの、貢献者は towncrier を利用せずに済むからです。

blurb は Python のために新規に作られたツールで、 towncrier と reno の間くらいです。 最終的に、 Python の NEWS をより良くしていくためには独自のツールをメンテナンスする価値があるという判断で blurb が選ばれました。

PyCon US までに移行を完了する事を目標に、 NEWSエントリの順序をだいたいで良いので時系列順にソートするために reST のコメント形式でメタデータを入れるなど、細部を詰めているところです。

プルリクエストのブランチに push --force(-with-lease) をするのをやめよう

git で commit --amendrebase -i origin/master が好きな人いますよね。僕も好きです。でも、一人しか見ていないブランチはともかく、プルリクエストを出しているブランチで push -f が必要な操作をするときはよく考えましょう。

このプルリクエスト を出した人が、レビューで指摘された部分を修正したのにその後なかなかレビューしてもらえないとMLでレビューアを募ったのですが、最初にレビューした人が指摘事項がちゃんと直ってるのか Github でレビューするのが面倒だから後回しにしていた事をレスして、 push -f を非推奨にしようという話になりました。

スレッド: Regarding reviewing test cases written for tabnanny module

Github 移行前には Rietvelt を使っていたのですが、そこでは貢献者が新しいパッチをアップロードしたとき、レビューアはパッチ全体をレビューすることも、前のバージョンのパッチとの差分だけをレビューすることもできました。

Github もプルリクエストの diff 画面で "Changes from -- Show changes since your last review" を選択すれば、前回レビューしたコミットとプルリクエストの最新のコミットの差分をレビューすることができます。ですが今回のプルリクエストの著者は commit --amendrebase -i origin/master でコミットをまとめてしまったようで、その機能が使えなくなってしまいました。

python/cpython リポジトリではプルリクエストをマージするときに利用できる方法を "Squash and merge" だけに限定しています。なのでどれだけプルリクエスト内の履歴が汚くても、それはマージするときに消えるので気にする必要はありません。一方で push --force が必要な操作は、レビューアがレビューしていたコミットを消して余計な負担を増やす危険があります。(これは push --force-with-lease オプションでも同じです。)

この件は Python にかぎらず Github 上で運用されている OSS に貢献する時すべてに当てはまる話です。特に "Squash and merge" やコミット単位のレビューは去年登場したばかりの機能なので、(私を含め)昔から Github に慣れている人もそういった機能が使えなくなるデメリットにあまり気づいていないかもしれません。私も commit --amendrebase -i origin/master を気軽に使ってしまう方だったので気をつけます。

Python 2 の命日

Exact date of Python 2 EOL?

Python 2 の Python Core Developer によるメンテナンスは 2020 年で終了します。しかし、 2020 年の何月何日かは決められていません。

pythonclock.org というサイトで Python 2 の EOL カウントダウンがされていますが、とりあえず 4/1 と仮定しています。 Python の開発者向けのドキュメントでは 1/1 を仮 EOL date にしていました。

この日付についてオフィシャルの予定日を決めない?という話題が持ち上がったのですが、 Python 2.7 のリリースマネージャーの Benjamin Peterson さんが最後の Python 2.7 をリリースした日が EOL ということで特定の日付は決められませんでした。

PyCharmのデバッガが40倍高速に

PyCharm debugger became 40x faster on Python 3.6 thanks to PEP 523

PyCharm のこの Blog 記事  が ML で紹介されました。 PyCharm 2017.1 & Python 3.6 の環境でデバッガが最大30倍速くなったようです。

Python 標準のデバッガ (pdbとその低レイヤーのbdb) は、 Python が提供しているトレーシング機能を使っています。これは毎行コールバックが呼ばれてその中でブレークポイントかどうかの判定を行っているので、かなりオーバーヘッドが大きくなります。

さて、Python 3.6 の目立たない新機能として、外部から CPython にパッチを当てることなく JIT を追加できるようにすることを目的にして、フレームを評価する関数を置き換えられるようなAPIが追加されました。これは Microsoft の Pyjion という Python に JIT を追加するプロジェクトからの提案です。

PyCharm はその新機能を使い、ブレークポイントを設定するときにバイトコードを埋め込む方式のデバッガを開発することでこのスピードアップを実現したようです。

ただし、どうやら pdb が遅い一番の原因はパフォーマンスに大きく影響する部分が Python で書かれているからのようで、 trace ベースのまま大幅なスピードアップを実現している pdb-clone というプロジェクトもMLで紹介されていました。

さておき、この API を提案してくれた Microsoft と、そのAPIを利用した高速デバッガを実用レベルに持っていった JetBrains はグッジョブです。

Python 3.6.1 におけるバイナリ互換性問題

Python 3.6.1 でビルドした拡張モジュールが 3.6.0 で利用できないという互換性の問題が発生してしまいました。

What version is an extension module binary compatible with

元になったのは bpo-27867 で、 __index__ という特殊メソッドを悪用することでメモリ不安全なコードが書けるという問題です。 (Python は ctypes を使う場合などを除いて普通はメモリ安全になるようにという指針で開発されています。)

この問題を修正する過程で、 PySlice_GetIndicesEx() という公開APIの実装が _PySlice_Unpack(), _PySlice_AdjustIndices() という内部APIに分解された上で、 Py_LIMITED_API というマクロが定義されていないときは PySlice_GetIndicesEx() という同名のマクロでAPI関数を覆って、そのマクロが内部APIを直接呼ぶことで呼び出しのオーバーヘッドが削減される形になりました。

ところが、この Py_LIMITED_API というマクロは普通 Python 3.4 と 3.6 のようなマイナーバージョンが違う CPython で互換性のあるAPI, ABIを利用するために利用されるもので、多くのライブラリの拡張モジュールはこのマクロを使わずに 3.6.0 と 3.6.1 のようなマイクロバージョンでの互換性を期待しています。

Python 3.6.1 で PySlice_GetIndicesEx を使った拡張モジュールをビルドすると、 3.6.1 で追加された _PySlice_Unpack, _PySlice_AdjustIndices を参照してしまうので、 3.6.0 ではロードできなくなってしまいます。

binary wheel を配布しているライブラリの開発者は、当面の対策として、次のようにしてマクロをキャンセルすれば 3.6.0 でも 3.6.1 でも存在する関数を直接呼び出せるはずです。 (または 3.6.0 でビルドするという手もあります)

#ifdef PySlice_GetIndicesEx
#undef PySlice_GetIndicesEx
#endif

なお、まだ報告が1件だけということもあり、今のところ 3.6.2 の緊急リリース予定はありません。

Python 3.6 の set のメモリ利用量のリグレッション

Program uses twice as much memory in Python 3.6 than in Python 3.5

あるアプリケーションで Python 3.6 のメモリ使用量が 3.5 の2倍になるという報告がありました。 3.5 だと メモリが32GB のマシンで普通に終わる処理が、 3.6 だとスワップを使いだして数十倍時間がかかるというのです。

再現コードは OpenCL など僕が利用経験のない依存関係が多く、再現するのに32GBのRAMを乗せたマシンが必要で、1回の実行に30分以上かかり、しかもアプリケーションはモジュールを動的に利用する形になっていてタグジャンプやgrepでソースを追うのも面倒という感じだったので、ほぼ1日がかりで問題の箇所を特定しました。

ちなみに、今回まず大まかにアタリをつけるために使った方法は、そのプログラムの最初で別スレッドを起動し、そのスレッドで30秒に一回メモリ使用量 (今回は swap 起こすので getrusage(2) ではなく /proc/self/statusVmRSSVmSwap を参照しました) とスタックトレースを表示するというものです。これでアプリケーションの構成に対する予備知識がほぼゼロの状態でも、プログラムのどの部分を実行するときにどれくらいメモリ使用量が増えているのかを大まかに把握することができました。

結果として 、 set (または forzenset) のコンストラクタ引数に set (または frozenset) を渡したときに、その set のメモリ使用量が増えてしまっていることが分かりました。 bpo-29949

set のコードを整理するときにミスがあり、 set.add(x) で要素を追加するときに内部で起こるリサイズ(set.add() が繰り返し呼ばれる可能性があるので、ハッシュテーブルがかなり余裕をもった大きさにリサイズする)と同じ計算が set をコピーするときに利用されてしまい、メモリ使用量がほぼ倍になるというリグレッションでした。すでに 3.6 ブランチで修正済みなので 3.6.2 で直るはずです。

Python 3.6.1 (default, Mar 23 2017, 16:39:01)
[GCC 6.2.0 20161005] on linux
>>> import sys
>>> s = set(range(10))
>>> sys.getsizeof(s)
736
>>> t = set(s)
>>> sys.getsizeof(t)
1248  # 同じ set なのに約1.7倍!

脱線ですが豆知識を紹介しておきます。 dict が1エントリに key と value の2値を記憶するのに対して set は1エントリ1値なので、同じ要素数なら set の方が省メモリになりそうに思えますよね?実は Python 3.6 では(このリグレッションを抜きにしても)メモリ使用量は逆転しています。

>>> d = dict.fromkeys(range(10))
>>> sys.getsizeof(d)
368  # set(range(10)) に比べて半分!

これは次のような理由です。

  1. ハッシュ値をハッシュテーブルのエントリに格納しているので、 1エントリあたりの大きさの比は 2 : 1 ではなく 3 : 2
  2. しかも compact orderd dict が採用されて dict のメモリ使用量が約 2/3 になっているので、ハッシュテーブルの大きさが同じならメモリ使用量は同等
  3. さらに set の方が dict よりもよりハッシュテーブルをアグレッシブに大きくするのでメモリ使用量が逆転する

ハッシュテーブルのサイズに関して dict と set で方針が異なるのには理由があります。dict は名前空間として利用されるので検索がヒットすることが多いのに対し、 set は集合演算として利用されるのでミスヒットが多いケースの重要度が dict よりも高くなります。ハッシュテーブルからの検索がヒットする時は衝突チェインの平均長の半分が探索コストの期待値になりますが、ミスヒットするときは衝突チェインの長さが期待値になるので、ざっくりと言えば衝突のコストが倍になるのです。そのため、より衝突を減らそうと set はハッシュテーブルをアグレッシブに大きくしているのです。

ハッシュテーブルの密度が低いということは、compact ordered dict と同じ方式を採用すれば省メモリ化できる余地が大きいということでもあります。ですが、今回のリグレッションの件でリグレッション以外の部分についてもハッシュテーブルのサイズのバランスについてあれこれ意見を言って、set職人のRaymondさんに「俺にゆっくり考える時間をくれ」と釘をさされてしまったので、また機を改めて提案してみます。


@metahne

songofacandy at 19:53
この記事のURLComments(0)TrackBack(0)Python 
2017年04月14日

Brocade VDX NOS 7.0 は Python が動く!

はてなブックマークに登録

KLab では物理のサーバインフラを複数システム運用していますが、最近そのうちの1つでネットワークスイッチのリプレースを実施しようとしています。新たに導入するスイッチは Brocade 社の VDX6740 なのですが、このスイッチの OS である NOS 7.0 では、なんとスイッチ上で Python を動かすことができます。ということで少し触ってみました。

とりあえず触ってみる

使い方は、 UNIX の shell から Python を使うのと同じ感覚です。まず、通常通りスイッチに ssh などでログインします。

$ ssh -l admin vdx-switch


Welcome to the Brocade Network Operating System Software
admin connected from 192.168.0.10 using ssh on sw01
sw01#
この状態で VDX の CLI コマンドを入力するのと同様に、pythonと入力すれば、python が対話モードで起動します。
sw01# python
Python 3.3.2 (default, Apr 28 2016, 15:57:52)
[GCC 4.3.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
見て分かるように、バージョンは 3.3.2 です(ちょっと古いですね…)。

探索

普段 Python のスクリプトを利用するのと同様に python コマンドの引数にスクリプトファイル名や -m でモジュール名を指定すれば、対話モードを起動するのではなく、指定した Python スクリプトを動かすこともできます。

sw01# python -m pydoc
pydoc - the Python documentation tool

pydoc ...
Show text documentation on something. may be the name of a
Python keyword, topic, function, module, or package, or a dotted
reference to a class or function within a module or module in a
package. If contains a '/', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
(略)

ライブラリ類はどうなっているのか、 python -m pydoc modulesの結果を整形したものを、Linux OS にインストールされていた同じバージョン 3.3.2 の Python のものと比べてみました。比較対象の Python 環境は概ね素に近い環境のはずです(ちょっと余計なものが見えてますが…)。左が VDX のもので、右が Linux のものになります。

2d1
< CLI
25a25
> _dbm
29a30
> _gdbm
36a38
> _lzma
103d104
< curl
137a139
> idlelib
147a150
> lib2to3
192d194
< pycurl
196d197
< pythonStarup
203d203
< requests
252a253,254
> turtle
> turtledemo
254a257
> unittest
1つめの CLI は、Pythonスクリプトから VDX の CLI コマンドを発行するためのライブラリです。それ以外では HTTP 関連のライブラリが追加されているようです…

さすがに setuptoolspip は入ってないので VDX の Python 環境内から新規パッケージを追加するのは無理そうですが、標準ライブラリの範囲内であればひと通り使えそうです。

ユーティリティの作成

ということで、早速ちょっとしたスクリプトを作ってみました。こちらで公開しています。

diff.py

スイッチの運用をする上で、同じ設定を複数のポートに施すことはよくありますが、本当に同じ設定になっているのか、という確認を CLI 上で行うには、人間の目で頑張ってチェックするしかなく、機械的行うにはスイッチの外に show コマンドの結果などをもっていって行う必要があります。しかし、毎回スイッチ上でのコマンドの実行結果を手元にコピーして比較するのは面倒でした。

Pythonの標準ライブラリには difflib があるので、diffコマンドぽいものを作ってみました。スイッチ上のファイル同士や引数に渡されたコマンド列の実行結果を使ってdiffを取ります。詳しい使い方は github にある README.mdを見て下さい。

sample__port_parse_and_compose.py

こちらは別に作ったスクリプトの、コマンド引数の解析部分を抜き出したものです。スクリプトに対して操作するスイッチのポート番号を指定するための引数を解析する argparse コードとチェック用関数、VDXスタイルのポート番号表現を組み立てる関数が入っています。VDX用のスクリプトを作成される際に活用できそうでしたら、作成されるスクリプトの目的に合うように修正しつつ、お使いください。

2017年04月04日

LVSの高負荷対策 その1 ~障害発生~

はてなブックマークに登録

こんにちは。インフラ担当の岡村です。
昨年、あるサービスで使用中のロードバランサが停止してしまうという事件が起こりました。
事の顛末を、数回に分けて紹介していきたいと思います。 もし同様の事象にお困りの場合は、役立てて頂ければと思います。

システム構成

KLabのDSASでは、ロードバランサにLVS (Linux Virtual Server) を使用しています。
ロードバランサはマスタ-バックアップ構成になっており、マスタ側が停止してしまっても、通常はバックアップ側がマスタに昇格し、サービスを継続できるようになっています。 おおまかな構成は下図のようになります。

dsas_lvs_network

障害発生

ある日の晩、突然ロードバランサ(マスタ側)の死活監視のアラート通知が届きます。

(なんだろう..。電源障害? その他HW障害? もしくはカーネルのバグを踏んだ?)

原因調査・復旧はもちろん必要ですが、冗長構成のため、とりあえずサービスは継続可能です...。そのはずでした。
数分後、今度は昇格した新マスタの死活監視アラートが届きます。

(2台揃って落ちた? 208.5日問題のようなバグ? 一体何が起こってる??)

緊張が走ります。
その後も何度かアラートが飛びましたが、サーバにログインする頃には止んでおり、ロードバランサは起動している状態でした。

調査

どうやら、マスタ側のロードバランサで再起動が発生し、バックアップ側がマスタに昇格するのですが、その昇格後にまた再起動が発生する、という事象が繰り返されていたようです。
ロードバランサのログを調査したところ、oom-killerが動いていたりでメモリ周りで問題が起こっていそうでした。

(ロードバランサログ)
・kernel: keepalived invoked oom-killer: gfp_mask=0xd0, order=0, oomkilladj=0
・kernel: IPVS: ip_vs_conn_new: no memory available.

もしメモリの枯渇が起きていたとすれば、32bitカーネルを使用していることが原因の一つである可能性がありました。32bitカーネルでは、カーネルが使用するLowメモリが制限されるためです。
再起動が起こる前に、Webサーバに対するヘルスチェックのエラーが頻発していることもわかりました。

(ロードバランサログ)
・Keepalived_healthcheckers: Timeout connect, timeout server [“WebサーバIP”:”ポート ”].

同時刻に、Webサーバでは次のログが出ていました。

(Webサーバログ)
・kernel : TCP: TCP: Possible SYN flooding on port "ポート番号". Sending cookies. Check SNMP counters.
SYNフラッディングで攻撃された?と頭をよぎりますが、断定はできません。
このログは、正常の通信でもWebサーバが捌ききれない量の通信が来れば出力されます。

「パケットキャプチャが欲しいね」
インフラチーム内からそんな声が聞こえてきました。

再発

> 「パケットキャプチャが欲しいね」
この願いはすぐに叶いました...。

障害の調査中に、またロードバランサが再起動を繰り返し始めたのです。

パケットの特徴

すかさずtcpdumpでパケットを観測したところ、以下の特徴を持つパケットが大半を占めていることがわかりました。

・synパケット
・tcp window size が0
・オプションなし

正常な通信においては、synパケットが大半を占めるのは考えにくく、tcp windows size が0かつオプションがない、というパケットもありえなさそうです。 観測できた範囲では、頻度は1秒間におよそ10万パケットで、送り元のIPアドレスはばらけていました。

緊急の対策

対策を行うために、検証環境でロードバランサに負荷をかけるなどして、ロードバランサが落ちた原因を究明したいところでした。しかし、さらなる再発に備えて、取り急ぎ対応を行う必要がありました。

Amazon CloudFront の利用

KLabではオンプレ環境だけではなく、AWSを使用してのサービス提供も行っており、クラウドチームから、CloudFront を間に挟んでみてはどうか、という提案をもらいました。 (DDoSに対するAWSのベストプラクティス) Amazon CloudFront も既に運用の実績があったため、スムーズに実現できました。

対策の効果

CloudFrontを挟んで以降、しばらく経過を観察したのですが、上で観測したような大量のパケットはLVSに届かなくなりました。観察は、特徴に当てはまるパケットが来たらカウントするように、ロードバランサに以下の設定を投入して行いました。

# iptables -t mangle -A PREROUTING -p tcp -m u32 --u32 "0x20&0xffff=0x0" -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m length --length 40
tcp window size が0という特徴は、u32モジュール(参考)を使用し、オプションがないという特徴は、--length 40 とすることで、マッチさせています。CloudFrontを挟んでもなおパケットが届く場合は、更にDROPターゲットを指定すれば、netfilterでのフィルタリングもできそうです。

今回の場合、CloudFrontによってフィルタリングされているのか、そもそも観測したようなパケットがもう飛んできていないのか、その判断はできないのですが、以後ロードバランサが落ちることはありませんでした。

めでたし、めでたし!


...ではなく、
次回は、ロードバランサ再起動の原因と、その対策をご紹介します!

okamura_h at 08:30
この記事のURLComments(0)TrackBack(0)lvs 
2017年03月16日

最近の Python-dev (2017-03)

はてなブックマークに登録

バックナンバー:

Python 3.6.1rc1

Python 3.6.1rc1 がリリースされました。大きな問題がなければ 3.6.1 は 3/20 にリリースされる予定です

3.6.1 は Github に移行してから初めてのリリースになります。なにか問題がないか確認するために、いつものRC版以上にソース形式・バイナリ形式両方の配布物のテストが必要なので、可能な方は協力をお願いします。

Github 移行後日談

以降から1ヶ月が経ちました。開発者からのフィードバックはおおむね好評です。私は、気軽にプルリクエストをくれた人がCLAにサインしないまま放置して大量のマージできないプルリクエストが貯まることを懸念していたのですが、今までののところそれも大丈夫そうです。

一方で Misc/NEWS といういわゆる changelog にあたるファイルが頻繁にコンフリクトを起こしていて解決する新ツールが開発中だったり、プルリクエストをマージするときのコミットログのフォーマットなどが話し合いでルール決まったけどまだドキュメント化されてなくて Github のデフォルトのままマージされるケースが多いとか、細かい問題はまだまだ続いています。

また、Travis-CI でのテストをパスすることをマージボタンを押すのに必須の設定にしているのですが、それがよく遅延する問題がありました。簡単な修正のバックポートでいちいち待ってられない。既に Travis-CI に (幾つかの Python 関連 organization 共有で) 並列25jobを割り当ててもらっていてこれ以上は望めない。どれかテストを削ろう。という話し合いをしていたら、「あ、並列25jobの件、今日アクティベートされたから。」という書き込みがあり、実際その日以降 Travis-CI のテストは快適になりました。まだアクティベートされてなかったなんて誰も知らなかったよ…とはいえ Travis-CI さんには100万の感謝を。

CPython 3.7 is now faster than CPython 2.7 on most benchmarks

いろんなアプリを集めたベンチマークで Python 2.7 と 3.5 を比べた時は速くなってるのと遅くなってるのが半々くらいだったのですが、 Python 3.6 やそれ以降の高速化によってそろそろ Python 3 の方が速いと胸を張って言える感じになってきました。具体的には10%以上遅くなったベンチが12件に対して、10%以上速くなったベンチが23件あります。

ちなみに遅くなってるベンチのトップ2は Python コマンドの起動 (`-S` オプションありなしの2種類) です。例えば Python 2 では組み込みの open 関数は C 言語の FILE* をラップしたオブジェクトを返していて、完全に Python インタプリタ内で実装されていたのですが、 Python 3 では FILE* が使われなくなり代わりに io モジュールが利用されるようになったため、起動時に io モジュールとその依存モジュールがインポートされるようになっているのが遅くなってる原因の1つです。

起動が遅くなったと言っても数ミリ秒ですし、起動時にインポートされるモジュールは利用頻度が高いモジュールなので、 Python 本体の起動は遅くなっていても Python で書かれたアプリの起動時間には(結局アプリがそのモジュールをインポートする可能性が高いために)影響しないことが多いはずです。

とはいえ起動は1ミリ秒でも早いに越したことは無いですし、中二病な vim プラグインで有名な方が Python の起動が (特に Lua と比べて) 遅いと愚痴をこぼしているのを Twitter で見る機会もよくあったので、 メンテナンスコストとのバランスが取れた範囲での最適化 をしているところです。

PEP 543 -- A Unified TLS API for Python

Python には昔から ssl という標準ライブラリがあるのですが、これは OpenSSL を Python から使うためのものです。

最近は一層TLSの重要性が増しているのですが、 Windows などで OpenSSL をバンドルして配布してる環境で OpenSSL のセキュリティーアップデートを配布する必要が出てきてしまいます。 macOS でも、 Apple がシステムの OpenSSL を deprecate してしまってもうちゃんとメンテしてくれてないので、 Windows と同じ問題が発生しています。 (PyPI が CDN に fastly を使っている関係で、 macOS のシステムの OpenSSL が対応してない TLS v1.3 が必須になるという一大イベントも最近ありました。)

OpenSSL をやめて macOS では SecureTransport, Windows では SChannel を使えば、 OpenSSL に依存せずに、 Apple や Microsoft がメンテナンスしてくれている証明書ストアとTLS実装を使うことができます。また最近は OpenSSL 以外の実用的なTLS 実装も増えてきています。 OpenSSL に依存せずに簡単に TLS を使えるような仕組みがあると多くの人が幸せになれます。

今回提案された PEP 543 は、そのはじめの一歩として、TLSのライブラリが実装するべき標準のAPIを定義して、 ssl モジュールにその API を実装させるというものです。これができれば、同じAPIに準拠した SChannel をラップするなどの別実装モジュールを PyPI で配布し、TLSを利用するライブラリは ssl とサードパーティライブラリの間でスイッチしやすくなるはずです。

提案はされたものの、実際に実装・利用することなくこれをレビューして議論できる人がいないので、試験的に実装して問題点がないか洗い出す方向で進んでいるはずです。

多分これが Python 3.7 で一番重要な新機能になるんじゃないかな。

Translated Python documentation

Python のドキュメントの翻訳が一番進んでいるのが、僕も参加している日本語訳(古い Python の changelog とか拡張を書く人しか必要としない Python/C APIのドキュメントも全部含めて85%以上!)で、次に翻訳が進んでいるのがフランス語(翻訳率は10%台だけどチュートリアルはカバーされている)です。

そんなフランス語の翻訳をされている Julien Palard さんが、 docs.python.org/<lang>/ 配下で翻訳ドキュメントを公開できるようにしたいと 去年 python-ideas MLで提案していて、僕も翻訳の管理、自動ビルド、ホスティングなんかの下回りを共通化すれば他の言語でももっと翻訳がやりやすくなるだろうなと思って応援しています。

さて、 Python のドキュメントは Sphinx というツールで構築されており、翻訳も Sphinx が持つ機能を使っています。具体的には Sphinx が英文をパラグラフ毎に分けて gettext の POT 形式で出力したり、そのパラグラフに対応する翻訳文を mo 形式のファイルから読み込んで英文と差し替えることで、他の gettext を使ったOSSと同じような手順で翻訳ができるようになっています。

本家のドキュメントはもちろんこの機能を使わずに構築されているのですが、この機能を使うと問題が出る部分があり、日本語プロジェクトでは fork に修正を当てた状態で利用しています。幾つかの修正は日本語以外の言語の翻訳でも共通して必要なものです。ということで本家で修正するプルリクエストを作ったのですが、ドキュメント翻訳に否定的なメンバーから Reject されてしまいます。しかしその後 Victor Stinner さん(この1年ではダントツでトップのコミッター) が Accept してくれて、形式上2対1になりました。

一応マージボタンを押してもいい状況ではあるものの、別に急ぐ必要はないし様子見で放って置いたら、 Victor さんがさらに「翻訳を支援しようぜ」っていうメールを Python-Dev に投稿してくれ、多くの賛同のレスが得られました。 Victor さんマジイケメン。

そんなこんなで、問題のプルリクエストはマージしました。これからは Julien さん達と協力して、日仏以外の Python コミュニティの人も翻訳しやすい仕組みを作っていけたらなと思います。

ちなみに、現在 Python のドキュメント翻訳は Transifex というサイト (OSS向けの無料プランあり) で行っているのですが、今後のことを考えて Red Hat が作っている Zanata を試してみました。 Transifex が持っている重要な機能はほぼカバーしているし、 Transifex には無いバージョン機能(翻訳文のバージョン管理じゃなくて、同じプロジェクトの原文を複数バージョン持てる)があったり、サポートのレスポンスもすごく早くて良かったです。 また、 gettext 等の形式ではなく Web サイトを WYSIWYG で翻訳するなら、 Mozilla の Pontoon が良さそうでした。 何か OSS で翻訳プロジェクトを始めようとされている方は参考にしてください。


@methane
songofacandy at 18:40
この記事のURLComments(0)TrackBack(0)Python 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
Syndicate this site