@ledsun blog

無味の味は佳境に入らざればすなわち知れず

マネジメント入門 その2

マネジメント入門 その1 - @ledsun blog につづいて 管理職必読 順番に読むと理解が深まる「マネジメントの名著」11冊 | 日経BOOKプラス で紹介されていた本の二冊名です。

1冊目同様、部長、課長くらいの日本企業の中間管理職向けの本です。 1冊目と比べると具体的な話が少なめでした。 1冊目で大枠をつかんで、2冊目で引き出しを増やす感じでしょうか。 「これしかやらない」というタイトルとは、あまり一致していないのかもしれません。

印象深かった内容

  • 部下は、数値目標よりも、それで得られるものを語って欲しい。
  • 最初の3年間に厳しい仕事を任せないと、4年目以降の成長が鈍る
  • 丁寧な指示のしかた
    • なぜその業務を頼むのか伝える
    • 具体的な手順を伝える
    • 指示を聞いてどう思ったかを確認する
    • 不安な点、不明な点がないか確認する
  • 任せる上司は部下の「不便・不安・不満」を事実で答えられる
  • 「やらされた仕事」では成長できない
    • 失敗した言い訳を他責にする
    • 工夫できない
  • 心が折られた人は、仕事を任せて結果を出させながら、折々の会話の中で他の視点があることも伝えて行く
  • 面白さを教えるのではなく、面白くする方法を教える
    • 仕事の流儀、譲れないこだわりを伝えると、「仕事を面白くする方法」も伝わる
  • いい人より格好良い人であること
    • 社外活動が充実している上司は、部下から魅力的にみえる
  • 上司が部下の未来に関心を持っているか?
    • 将来の夢、やりたいこと、なりたいものを話し合う機会があるか?
  • 優秀な若者は「今の給与以上に成長できること」を報酬と考える
  • 普通に生活しているだけでは「自分はどうしたい」を考えない。
    • 気がつくきっかけを上司が与える
    • 仕事で大事したい価値観を5つあげてもらう。その中の1位を選んでもらう
    • 価値観の背景を聞く
  • チームのNo.2が賛成すると、合意が早い
  • 感謝する機会を計画的に仕込んでおく
  • 失敗を恐れる人と恐れない人の差は、勇気の差でなく、見ている期間の差
  • やらないことを決める。それが経営だ
  • 賛成してくれる2割の意見を聞きながら、進め方を決め、6割に役割を与えながら巻き込んでいく
  • 孤独を感じたら、本に答えを求める
  • 多様な人たちとの接点を通じて、それぞれの価値観を自分の価値観としてインストールする経験があるとよい

「自分がどうしたい」を考えないとか、「仕事を面白くする方法を知らない」とか、驚きです。 そういう人が居るのは知っていました。 そういう人を導いていくことがマネジメントに含まれるとは思っていませんでした。

エンジニアコミュニティにはそういう人少ないのです。 創立20年くらいの小企業にも少ないです。 いや、そうではなく、そういう人達は生き延びることができなくて退場していきます。

言われてみれば「そういう人達をモチベートして、退場しないで戦力にできる。戦力にできたら強い組織になる」のは、そらそうだなっと思いました。 選ばれた特質を既に備えている人を選別するのではなくて、備えられるように育てる。 そら、まっとうな話ですね。

2冊読んで2冊とも似たようなことが書いてあるので、まあ、そうなんだろうなって思います。 まじかあ・・・

マネジメント入門 その1

管理職必読 順番に読むと理解が深まる「マネジメントの名著」11冊 | 日経BOOKプラス で紹介されていた本です。 とりあえず一冊目を読んでみました。

マネジメントの本は経営者からチームリーダー向けまでターゲットがわかりにくいことがあります。 この本は、部長、課長くらいの日本企業の中間管理職向けの本です。

1 on 1の話は3章からはじまります。 1~2章をつかってマネージャーの仕事を説明しています。 100ページくらいにコンパクトにまとまっていてわかりやすかったです。

1 on 1 の具体的なハウツーは3章に書いてあります。 僕の所属組織は筆者の方の所属組織とは形が違うのでハウツーをそのままでは使いにくいです。

4章以降も1 on 1と絡めてはいますが、マネージャーの仕事全般の話で興味深かったです。

印象深かった内容

  • 部下に、今の仕事の延長線上に、将来なりたい自分がいることをイメージさせる
  • 部下の言う不満を代わりに解決するのではなく、解決のためにできることのアイデア出しを手伝う
  • 部下に(目標管理上の)期待する行動や成果を伝える
  • 1 on 1 で、マネージャーに「改めて欲しいこと」「直して欲しいこと」「マネージャーとして不足していると感じるところ」を聞く
  • 毎月一回、自分のオススメ本を3分で紹介するイベント
  • 部下の成長は「仕事から得られる実体験」が7割
  • 任せた仕事を投げ出したり、妥協しようとしたら、とがめる
  • 数値目標は再現がないのでミッションをつくった
  • 能力の低い部下の方が、一度に伸ばせる能力の幅は小さい
  • ベテランを外回りの営業からチーム内全員の事務作業担当にコンバートした
  • Z世代では「出世に興味ない」というが、「仕事のできるかっこいいビジネスパーソンになりたい」と思ってたりする
  • スキルを身につけるには回数が大事。一回で上手くいかなくても、我慢して見守る
  • 若者は、大量の情報を得られる。情報を取捨選択する社会経験が足りていない。混乱して01で判断しがち。解像度高く理解できるようにサポートが必要
  • 1 on 1 で初心を思い出させる
  • 本当に困った部下を育てる方法が載った本はない

この中でも特に自分に足りていない点は、次の2点かなと思います。

  • 組織の目指す方向をくわしく説明する
  • 各メンバーの今の仕事と組織の方向が一致している事を説明する

本当に、サーバントリーダーシップというか、部活のマネージャー的なマネージャーなんだなと、思いました。 新人マネージャーが一冊目に読む本として、内容の網羅具合もページのコンパクトさも良さそうです。

開発チームの人数とふるまいイメージ

チームの自律性

お仕事でソフトウェアを開発するプログラマーチームに対して、僕が持っているイメージを整理しました。

人数
2 リードとサブ コンビ
3 リードとサブ x 2 コンビとサポート チーム
4 リードとサブ x 3 チームとサポート チーム
5 マネージャーとメンバー x 4 チームとサポート コミュニティ

この表は、右に行くほどチームが自律性高くふるまうことをあらわします。

自律性が高い方が良いとは限りません。 自律性が低いと5人以上のチームでは、チームマネジメントを専任する人が必要になると思います。 それをマネージャーとメンバーと表現しています。 プログラマーの中にはチームマネジメントを専任したくない方が一定数います。 もしも、プログラマーからチームリーダーになり、チームマネジメント専任になりたくない場合、チーム人数が少ないうちから自律性を高めておくのがオススメです。

チームの人数が増えてから自律性を高めるには時間が掛かります。 チームメンバー同士が対話する機会を設け練習を繰り返す必要があります。 週1回程度の話し合いの機会を半年~一年くらい続けると、自律性が高まるように思います。

チームの自律性を重視するときに注意事項があります。 2人チームがリードとサブになるかコンビになるかは、性格の相性に強く依存します。 2チームでは、自律性をあまり重視しない方が無難です。

一方、3~4人では自律性を重視すると良いと思います。 リードとサブ x n の状態で人数が増えると、リーダとチームメンバーのコミュニケーションが増え、チームメンバー同士のコミュニケーションが増えません。 リーダーがコミュニケーションにつかうコストが増え、チームメンバー同士のコミュニケーションを増やす施策が打てなくなります。 チームの人数が増えるとチームのパフォーマンスは一度下がります。 チームのパフォーマンスを上げるために、リーダとチームメンバーのコミュニケーションに時間を使いたくなると思います。 一度パフォーマンスが上がってからチームの自律性を高めようとしても、チームの自律性を高めるには再びパフォーマンスを一時的に下げる必要があります。 怖くてできません。 「パフォーマンスが上がっているし、自分が我慢すれば良い」と思いがちです。 人間は「自分が我慢すればよい」誘惑に抵抗するのに、ものすごいパワーが要ります。

コミュニティ

上の表をみると、5人チームにコミュニティという謎の状態があります。 チームとコミュニティの違い、会社・組織をどう捉えるか | Social Change! に出てきているコミュニティをイメージしています。 なぜ、チームではなくコミュニティなのかというと、5人以上になると、ミッションに対して100%動いていないメンバーがでてきます。 4人のチームはめちゃくちゃ上手く行っていると4人分の成果がでます。 5人のチームはめちゃくちゃ上手く行っても4.5人分の成果です。

スキルの差からではなく、タスクの投入量が上手くコントロールできなくなるために起きます。 手の空くメンバーは特定の人物ではありません。時々によって手が空くメンバーは変わります。 タスクを上手く用意すれば5人分の成果がでるようになるかというと、僕の経験則では、なりません。

チームにミッション以外のことに取り組む余裕が生まれた状態と捉えるとよいです。 ミッションとは別の緊急ではないが重要なタスクを用意しておいて、チームメンバーに取り組ませるとよいでしょう。 これはチームの余裕です。 4人までのチームでは緊急ではないが重要なタスクは正式なタスクにしたり、リーダーが空き時間でこっそりやったりという工夫が必要です。 5人以上のチームがコミュニティになると、チームメンバーが自主的に緊急ではないが重要なタスクに取り組めるようになります。

また、もともと4.5人分の成果しか出ないので、メンバーの離脱や加入が容易になります。 一人離脱しても12.5パーセントしか下がらないのでインパクトが弱いです。 一人加入にしても1人分の成果は期待されないので、新メンバーの負担が低いです。 このメンバーの出入りが容易な状態が、チームではなく場でありコミュニティです。

チームの自律性を高めるコツ

チームの自律性を高めるコツは、チームメンバーに縄張りを作らせないことです。 人間は縄張り意識を持つと、その作業を他人にされるのが苦痛に感じます。 また、縄張り外の仕事するのがおっくうになります。

縄張りをつくらせないためには、リーダーはチームメンバーの扱いを変えてはいけません。 チームメンバーのスキル、年齢、性別、得意なことが違っても扱いを変えません。 経歴20年のベテランと新卒半年の新人の扱いを同じくします。 スキルが足りない部分のサポートは必要です。 スキルが足りている作業は平等に割り振ります。

チームメンバーによって実行速度の遅い速いの差はありますが、クリティカルパスに乗っていない作業は早さを気にする必要はありません。 また時間が掛かっても成果を出せた体験は、個人の成長に繋がります。 さらにクリティカルパスに乗っていない作業は早さを気にする必要が無いは頭で理解しても、なかなかその通りにふるまえません。 何度も実際に体験する必要があります。

平等に割り振らずに、ベテランにばかり難しい作業を振ると「この領域はおれの仕事」と縄張りを作りはじめます。 ベテランほど陥りやすいので、意図的にコントロールする必要があります。 コントロールせずに放置しておくと、よほどの人格者でないとブリリアントジャーク化します。 すでにブリリアントジャーク化していてコントロールできず、 短期的な成果よりチームの成長が重要な場面では、チームから外れてもらうことを考えてもいいかもしれません。

継続して開発していると、前回機能Aを担当した人に今回も機能Aをお願いしたくなります。 よほど急ぎでない限り担当はグルグル変えるようにしましょう。 繰り返しですが、クリティカルパスに乗っていない作業は早さを気にする必要はありません。 プロジェクトの進行が早くならないのに、縄張り作りを強化するのは、なにも得しませんよね。

MySQLのロックに関する調査メモ その4

MySQLのロックに関する調査メモ その3 - @ledsun blog の続きです。

SHOW ENGINE INNODB STATUS\Gで出力されるロック情報を眺めていたら意味がわかってきました。

まずテーブルに格納されているレコードの情報です。

 select * from mysqlcasual;
+----+------+------+
| id | col1 | col2 |
+----+------+------+
|  1 |    2 |    0 |
|  2 |    4 |    0 |
|  3 |    6 |    0 |
|  4 |    8 |    0 |
|  5 |   10 |    0 |
|  6 |   12 |    0 |
|  7 |   14 |    0 |
|  8 |   16 |    0 |
+----+------+------+

1つ目のレコードを選択

begin;SELECT id, col1 FROM mysqlcasual WHERE col1 = 2 FOR UPDATE;

三つのロック情報が出てきます。 一つ目から見ていきます。

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7731 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000001; asc     ;;

index idx_col1 of table はインデックス idx_col1 に対するロックであることを表しています。 lock_mode Xネクスキーロックであることを表しています。無印がネクスキーロックです。

 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000001; asc     ;;

はロックしているレコードを表してます。 80000002はcol1カラムの値が2、80000001はidカラムの値です。

選択するレコードを変えてみます。

2つ目のレコードを選択

begin;SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 FOR UPDATE;
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000004; asc     ;;
 1: len 4; hex 80000002; asc     ;;

8000000480000002になりました。

次のロック情報を見てみましょう。

2つ目のロック情報

RECORD LOCKS space id 173 page no 3 n bits 80 index PRIMARY of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e011d; asc        ;;
 3: len 4; hex 80000004; asc     ;;
 4: len 4; hex 80000000; asc     ;;

index PRIMARY of tableとあるので、プライマリーインデックスへのロックです。セカンダリーインデックスをロックするとプライマリーインデックスもロックされるやつですね。 lock_mode X locks rec but not gapは、レコードロックを表してます。ギャップロックでないものがレコードロックです。

 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e011d; asc        ;;
 3: len 4; hex 80000004; asc     ;;
 4: len 4; hex 80000000; asc     ;;

800000028000000480000000はレコードの値を表していそうです。 00000000162aaa0000011e011dは謎です。プライマリーインデックスとidex_col1を表しているのかもしれません。 上は2つ目のレコードです。1つ目のレコードは下になります。

 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e0110; asc        ;;
 3: len 4; hex 80000002; asc     ;;
 4: len 4; hex 80000000; asc     ;;

00000000162aは変わりません。 aa0000011e011daa0000011e0110になりました。 うーん、これはちょっとよくわかりません。

3つ目のロック情報

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000006; asc     ;;
 1: len 4; hex 80000003; asc     ;;

index idx_col1 of tableなのでidx_col1へのロックです。 lock_mode X locks gap before recなので、ギャップロックです。

 0: len 4; hex 80000006; asc     ;;
 1: len 4; hex 80000003; asc     ;;

は、3つ目のレコードを表しています。 ギャップロックなので、3つ目のレコードの手前までをロックしているようです。

もしかして、1つ目のネクスキーロックを、2つ目のレコードロックと3つ目のギャップロックで実現していることを表現しているのでしょうか?

MySQLのロックに関する調査メモ その3

MySQLのロックに関する調査メモ その2 - @ledsun blog の続きです。 概念だけ勉強しても理解に限度があります。 実際にロックを起こして観察してみました。

InnoDBのロックの範囲とネクストキーロックの話 - かみぽわーる を参考にしました。 MySQL Shellを起動しMySQLに接続します。

\connect --mysql --user root
\use sandbox
\sql

sandboxスキーマは練習用に作ってあったものを使い回しています。 テーブルを作成します。

CREATE TABLE `mysqlcasual` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `col1` int(11) NOT NULL,
  `col2` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_col1` (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `mysqlcasual`(`col1`) VALUES (2),(4),(6),(8),(10),(12),(14),(16);

MySQL 5.7 以降では、ここからの手順が変わっています*1

CREATE TABLE innodb_lock_monitor(a int) ENGINE=InnoDB;

の代わりに

SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;

します。 また、共有ロックではロックが表示されませんでした。

BEGIN;
SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 LOCK IN SHARE MODE;

の代わりに

BEGIN;
SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 FOR UPDATE;

を試しました。

SHOW ENGINE INNODB STATUS\G

を実行するとTRANSACTIONSセクションにロック情報が表示されました。

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000004; asc     ;;
 1: len 4; hex 80000002; asc     ;;

ネクスキーロック

RECORD LOCKS space id 173 page no 3 n bits 80 index PRIMARY of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e011d; asc        ;;
 3: len 4; hex 80000004; asc     ;;
 4: len 4; hex 80000000; asc     ;;

インデックスレコードロック?

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000006; asc     ;;
 1: len 4; hex 80000003; asc     ;;

ギャップロック?

出力された情報の読み方が謎です。

iroshizuku<色彩雫> 冬柿

万年筆のインクです。

赤っぽいのかと思っていました。 茶色とオレンジの間ぐらいの色でした。 好きな感じの色です。

冬柿で書いた文字の写真

写真で見ると、赤ですね。

このメモは別の資料を見ながら書いたメモです。 メモだけ見ても意味が通じないと思います。

MySQLのロックに関する調査メモ その2

MySQLのロックに関する調査メモ その1 - @ledsun blog の続きです。

MySQLのロックについて - SH2の日記 のPDFを読みました。 面白かったです。

MySQL(というかInnoDB)の最初のスタート地点が「絶対にファントムリードさせたくない」なようです。 つまり一度トランザクションをはじめたら、他のトランザクションでの操作を見たくありません。 例えば、自トランザクションで見ている行と行の間に、他トランザクションからInsertされたくありません。 ということは行単位のロックでは足りなくて、自トランザクションで見ている範囲をロックしたいです。 これが、ギャップロックとネクスキーロックのようです。

今の段階ではギャップロックとネクスキーロックの違いはわかっていません。 特に、なぜネクスキーロックをしなければいけないのかよくわかりません。

どうもこの辺の思想は「トランザクション処理 概念と技法」のでしょうか?

もう一つ、MySQLの面白い特徴は、プライマリーインデックスのリーフに行があることです。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.6.2.1 クラスタインデックスとセカンダリインデックス

すべての InnoDB テーブルは、行のデータが格納されているクラスタ化されたインデックスと呼ばれる特別なインデックスを持っています。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック に次のように書いてあります。

レコードロックは、インデックスレコードのロックです。

どうもこういうことを説明しているみたいです。

また、セカンダリーインデックスをロックするときは、セカンダリーインデックスとプライマリーインデックスのインデックスレコードを両方ロックするそうです。

MySQLのロックに関する調査メモ その1

MySQLのロックのついては MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック に書いてあるはずです。 読んでもいまいちよくわかりません。

MySQL/MariaDBとTransactdのInnoDBロック制御詳細 その1 - BizStationブログを合わせて読むとわかりやすいです。 どうやら公式ドキュメントは内部実装の立場で説明しているようです。 たとえば、後者には次のような記述があります。

2つのトランザクションが同じレコードのロックを取得しようとしたときのロックの可否を表したのがInnoDBソースコードの以下の部分です。

このソースコーコメントをマトリックスにしたものが公式ドキュメントに載っています。 ですので、公式ドキュメントは実装者視点で読む必要があります。 利用者視点で読むと理解が難しいようです。

後者の文章には、実装者視点の解説があるので理解の手引きになります。

Windows Formの同期イベントハンドラーから入れ子になった非同期関数を呼ぶとデッドロックする

await と Task.Result によるデッドロックによるとWindows FormでTaskを使ったときにデッドロックするケースがあるようです。 検証してみます。

デッドロックするソースコード

試しに次のようなコードを書いてみました。

namespace AwaitDeadLockEight
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            label1.Text = "";
            string str = TimeCosumingMethod().Result;
            label1.Text = str;
        }

        private async Task<string> TimeCosumingMethod()
        {
            await Task.Delay(3000);
            return "TimeCosumingMethod の戻り値";
        }
    }
}

ボタンを押すと確かに固まります。 await Task.Delay(3000);コメントアウトすると動きます。

なので、非同期関数を呼ぶだけでは問題ないです。 非同期関数のなかで非同期関数を呼ぶと固まります。

Task.Delay(3000).Wait();でも固まります。 awaitを使うかどうかは関係ないようです。

デッドロックしないパターン

次のように呼び出すイベントハンドラーを非同期関数にすると期待通りに動きます。

private async void button1_Click(object sender, EventArgs e)
{
    label1.Text = "";
    string str = await TimeCosumingMethod();
    label1.Text = str;
}

private async Task<string> TimeCosumingMethod()
{
    await Task.Delay(3000);
    return "TimeCosumingMethod の戻り値";
}

不思議な挙動です。何が起きているのでしょうか?

今のところの仮説

ここからさきは、今のところの仮説です。 イマイチ上手く説明できていませんが、現在の理解を書いておきます。

  1. .NETにはSynchronizationContextというものがあり、非同期関数の処理結果をUIスレッドのコンテキストに戻している
  2. コンテキストを戻すとは、UIスレッドのイベントループのタスクキュー(?)にタスクを積んでいる
  3. スレッドは自タスクキューをLIFOで処理する
  4. 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる
  5. 「TimeCosumingMethodの結果を処理する」タスクBがUIスレッドのタスクキューに積まれる
  6. LIFOでタスクBから処理しようとするが、タスクAが完了していないので、デッドロックする

でも、この説明だとイベントハンドラーを非同期関数にしたときに上手く動く理由が説明できません。 そもそも 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる は本当でしょうか? ワーかスレッドのキューに積まれるないのでしょうか? 謎です。

参考

ruby.wasmのJS::Object#newにブロックでコールバック関数を渡す

ruby.wasmのKernel#sleepをどう実装したものか? - @ledsun blog の続きです。

次のようにruby.wasmで動くKernel#sleepを実装できます。

<html>

<head>
  <title>Kernel#sleep</title>
  <script
    src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-26-a/dist/browser.script.iife.js"></script>
  <script type="text/ruby" data-eval="async">
    require 'js'

    module Kernel
      def sleep(time)
        JS.eval("return new Promise((resolve) => setTimeout(resolve, time * 1000))").await
      end
    end

    start = JS.global[:Date].now.to_i
    sleep(1)
    puts "#{JS.global[:Date].now.to_i - start}ms passed"
  </script>
</head>

</html>

Promiseの初期化を次のように書いています。

JS.eval("return new Promise((resolve) => setTimeout(resolve, time * 1000))").await

次のようにnewメソッドにコールバック関数をブロックで渡したいです。

JS.global[:Promise].new do |resolve|
  JS.global.setTimeout(resolve, time * 1000)
end.await

これは次のエラーがでます。

TypeError: Promise resolver undefined is not a function

しかしnewメソッドの定義はブロックを考慮していません。

def new(*args)
  JS.global[:Reflect].construct(self, args.to_js)
end

つまり次のJavaScriptを実行しているのと同じです。

new Promise(undefined)

ですので、resolver引数がundefinedであると怒られます。 では、次のようにしてはどうでしょうか?

class JS::Object
  def new(*args, &block)
    JS.global[:Reflect].construct(self, block)
  end
end

module Kernel
  def sleep(time)
    JS.global[:Promise].new do |resolve|
      JS.global.setTimeout(resolve, time * 1000)
    end.await
  end
end

newメソッドでブロックを引数として渡します。 上手く行きそうですが、やはり同様のエラーが起きます。

TypeError: Promise resolver undefined is not a function

これはJavaScriptでかくと、次のようになります。

Reflect.construct(Promise, ()=>{})

Reflect.constructの第二引数には、配列風オブジェクトを渡す必要があります。 次のようにすると良さそうです。

def new(*args, &block)
  JS.global[:Reflect].construct(self, [block])
end

ひとまず動きます。 つぎのようにすると、newメソッドにブロックを渡したときも、引数を渡したときもJavaScriptコンストラクターを呼び出せるようになります。

<html>

<head>
  <title>Kernel#sleep</title>
  <script
    src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-26-a/dist/browser.script.iife.js"></script>
  <script type="text/ruby" data-eval="async">
    require 'js'

    class JS::Object
      def new(*args, &block)
        if block
          JS.global[:Reflect].construct(self, [block])
        else
          JS.global[:Reflect].construct(self, args)
        end
      end
    end

    module Kernel
      def sleep(time)
        JS.global[:Promise].new do |resolve|
          JS.global.setTimeout(resolve, time * 1000)
        end.await
      end
    end

    start = JS.global[:Date].now.to_i
    sleep(1)
    puts "#{JS.global[:Date].now.to_i - start}ms passed"

    p JS.global[:Date].new("2024-01-27T00:00:00Z")
  </script>
</head>

</html>

オートローダー設計調査

ruby.wasmでオートロードする - @ledsun blogModule#const_missingをつかってオートローダーを書いてみました。 Module#const_missingAPIで、汎用的なオートローダーを書くのは少し難しそうなことがわかりました。 難しそうですが、どれくらい難しいのかよくわかっていません。 そこで、先人の知恵に頼ります。

具体的には、Rails 5.2 から6.0でオートローダーをclassicからzeitwerkに変更した出来事を調べます。 Rails 5.2までのオートローダーはModule#const_missingを使って実装されています。 Rails 6.0以降のオートローダーzeitwerkはKernel.#autoloadを使って実装されています。 なぜでしょうか?

ClassicローダーからみるModule#const_missingを使ったオートローダーの問題点

classicローダーのModule#const_missingを使った実装は、おおむね動いていましたが細かい問題がありました。 わかりやすい例は Understanding Zeitwerk in Rails 6 | by Marcelo Casiraghi | Cedarcode | Medium で、説明されています。

次のような例が挙げられています。

# app/models/user.rb
class User < ApplicationRecord
end

# app/models/admin/user.rb
module Admin
  class User < ApplicationRecord
  end
end

# app/models/admin/user_manager.rb
module Admin
  class UserManager
    def self.all
      User.all # Want to load all admin users
    end
  end
end

UserManager内でUserを呼び出したときに、Module#const_missingでは、呼び出された物がUserかAdmin::Userかわかりません。 Module#const_missingの引数には、どちらの場合であってもシンボル:Userが渡されます。

その他の多くの問題がRails 5.2のRailsガイドで列挙されています。 Module#const_missingでオートローダーを実装するには、とても多くの問題があるようです。

Zeitwerkのとった解決方法

これらの問題を解決するためにzeitwerkはKernel.#autoloadを使って再実装されました。 Kernel.#autoloadはモジュールを読み込むパスを指定できます。 モジュールが必要になったときに指定したパスをつかってKernel.#autoloadします。 つまり、オートロードです。 正確にはオートロードに使うヒントをRuby VMに渡します。

zeitwerkでは、起動時にsetupメソッドを使ってルートディレクトリからモジュールのファイルパスを収集し、Kernel.#autoloadを呼び出します。 Kernel.#autoloadを使うには、モジュール呼び出しより前に、モジュールの実体ファイルのパスを調べます。

ruby.wasmへの応用を考える

ruby.wasmでは、この方式は厳しいです。 サーバーに対してパス探索をかけたくありません。 自動的にモジュールのパスを集めるのは難しいです。

さらに前の段階でモジュールパスを調査しておいて、import mapsのようにブラウザに渡せばできるでしょう。 ですが、オートロードがうれしいのは設定が要らない点です。設定より規約(Convention over Configuration、CoC)です。 マップを作ってブラウザに渡すのは設定です。 Kernel#require_relativeを各Rubyスクリプトファイルに書くのと同じです。

参考

ruby.wasmでオートロードする

kakikataというペン習字練習用紙を印刷するruby.wasmアプリケーションがあります。

ledsun.github.io

複数ページ印刷する機能を追加するついでに、requrie_relativeを使ってファイル分割していました。 やっている内に、単純作業が面倒臭くなってオートローダーが欲しくなりました。 const_missingを使えばできるはずと思って、ひとまず作ってみました。

簡単オートローダー

https://github.com/ledsun/kakikata/blob/9fefffc518025fb559e4b5d7d5c07592b3f72a12/main.rb#L12-L31

# 定数名からモジュールをオートーロードします。
def Object.const_missing(id)
  module_name = id.to_snake_case
  JS::RequireRemote.instance.load(module_name)
  p "#{module_name} loaded!"

  mod = const_get(id)
  # 読み込んだモジュールに、サブモジュールのオートーロードを定義します。
  mod.define_singleton_method(:const_missing) do |sub_id|
    path = self.name.to_s.split('::')
                         .map(&:to_snake_case)
                         .join('/')
    module_name = "#{path}/#{sub_id.to_snake_case}"
    JS::RequireRemote.instance.load(module_name)
    p "#{module_name} loaded!"

    const_get(sub_id)
  end

  mod
end

初めて使う定数の読み出し時にconst_missingが呼ばれます。 定数名からRubyスクリプトのファイルパスを解決して読み込みます。 名前空間入れ子になっている場合は、親モジュールのconst_missingが呼ばれます。 モジュールを読み込んだときに、同様のRubyスクリプト読込処理を定義しています。

今回は比較的シンプルなアプリケーションなので、ルートディレクトリからのロードしか考慮していません。 ディレクトリ内のRubyスクリプトからロードしようとすると、ミスるはずです。 絶対パス指定にすれば良いだけのような気もしますが、試していません。

組み込みのも外部のもgemの読込は考慮していません。 erbをつかっていますが、手動でrequire 'erb'しています。 アプリケーションのコードをネーミングルールに従って読み込みます。

これだけの行数でオートローダーがかけるRubyって、不思議なプログラミング言語ですね。

以前はオートローダーは考えてなかった

僕が、もともと想定していた、ruby.wasmアプリケーションの作り方は、

  1. CRubyで大まかな動きを実装する
  2. ruby.wasmに移植する

でした。 Wordle SearchGitHub - ledsun/tetris: Rubyでテトリスを実装するは、このパターンです。 このパターンでは、オートローダーは要らないと考えていました。

Rails以外のCRubyアプリケーションを作る時にzeitwerkのようなオートローダーを使うのはあまり流行っていなさそうです。 Railsruby.wasmで動かしても、そんなに嬉しくなさそうです。 また、zeitwerkそのものをruby.wasmに持ってくるのは難しいです。 というわけで、オートローダーはオマケぐらいに考えていました。

kakikataは、ブラウザで印刷するためのアプリケーションなので、最初からruby.wasm向けに作っています。 CRuby版は存在しません。 CRubyでの開発体験を気にせずに、ruby.wasmではオートローダーが使えてもいいのかもしれません。

追記

よくると、このオートローダーは、動きがちょっと変です。

https://github.com/ledsun/kakikata/blob/9fefffc518025fb559e4b5d7d5c07592b3f72a12/app/document.rb

Documentはトップレベルのモジュールです。 Appモジュール内で呼び出されたので、App::Documentとして解釈しています。 ディレクトリがappなので解決できています。

リモート呼び出しだと、スカって次の候補を探すのはあまりやりたくありません。 もうちょっとシンプルなモジュール名-パス解決ルールを決めないと、汎用的なオートローダーとするのは難しそうです。

万年筆

万年筆を買いました。 ペン習字をはじめた - @ledsun blog のの一環です。 練習するなら良い道具を使った方が気持ちが盛り上がります。

以前、パイロットの細字を使っていました。 今回は万年筆のヌルヌル感を求めて太字にしました。 色はアイボリーです。 なんとなく、女性向けラインのような気はします。 形がスタンダードなので、色までスタンダードだとつまらないので、アイボリーです。

実際に書いてみると、漢字はかなり大きく書かないと潰れます。 書き慣れていないので、バランス調整が難しいです。

万年筆を使って書いた文字の写真

内容は辰年にちなんだ書き初めです。 サビは慣れちゃっているのでそうでもないのですが、すごいフレーズです。

愛遠き世界にひるがえす 純情にも似た欲望よ
銀色の月に照らされたオマエのEYEが怖いほど

文の意味はよくわかりませんが、あふれ出る中2感が素晴らしいです。 何をどうやったらこんな詞が書けるのでしょうか?

rustのWASI用のThreadのsleep関数を読む

https://github.com/rust-lang/rust/blob/e9271846294c4ee5bd7706df68180320c0b5ff20/library/std/src/sys/wasi/thread.rs#L137 *1

   pub fn sleep(dur: Duration) {
        let nanos = dur.as_nanos();
        assert!(nanos <= u64::MAX as u128);

        const USERDATA: wasi::Userdata = 0x0123_45678;

        let clock = wasi::SubscriptionClock {
            id: wasi::CLOCKID_MONOTONIC,
            timeout: nanos as u64,
            precision: 0,
            flags: 0,
        };

        let in_ = wasi::Subscription {
            userdata: USERDATA,
            u: wasi::SubscriptionU { tag: 0, u: wasi::SubscriptionUU { clock } },
        };
        unsafe {
            let mut event: wasi::Event = mem::zeroed();
            let res = wasi::poll_oneoff(&in_, &mut event, 1);
            match (res, event) {
                (
                    Ok(1),
                    wasi::Event {
                        userdata: USERDATA,
                        error: wasi::ERRNO_SUCCESS,
                        type_: wasi::EVENTTYPE_CLOCK,
                        ..
                    },
                ) => {}
                _ => panic!("thread::sleep(): unexpected result of poll_oneoff"),
            }
        }
    }

このファイルが何かはあまりよくわかっていません。 src/sysディレクトリにunixwindowsがあります。 rustでクロスコンパイルするときに、ターゲットOSに合わせて埋め込まれる実装だろうと思っています。

なんとなく読み方がわかったのでメモしておきます。

poll_oneoff in wasi - Rustpoll_oneoff関数のリファレンスがあります。

pub unsafe fn poll_oneoff(
    in_: *const Subscription, 
    out: *mut Event, 
    nsubscriptions: Size
) -> Result<Size, Errno>

実際の呼び出しが

wasi::poll_oneoff(&in_, &mut event, 1);

です。引数を3つ受け取っています。

  1. _in
  2. event
  3. 1

_inは少し上で定義している構造体です。

let in_ = wasi::Subscription {
  userdata: USERDATA,
  u: wasi::SubscriptionU { tag: 0, u: wasi::SubscriptionUU { clock } },
};

APIリファレンスとSubscriptionという型名が一致しています。 SubscriptionにはSubscriptionUと言う構造体が入っていて、さらにその中にSubscriptionUU構造体があります。 その中にclockが入ります。 clock

let clock = wasi::SubscriptionClock {
  id: wasi::CLOCKID_MONOTONIC,
  timeout: nanos as u64,
  precision: 0,
  flags: 0,
};

です。 要するにこの関数はpoll_oneoffを呼ぶのに必要なデータを作って、poll_oneoffを呼び出しています。 CLOCKID_MONOTONICというのは

CLOCK_REALTIMEとCLOCK_MONOTONIC #Linux - Qiita

CLOCK_MONOTONIC 時刻はかならず単調増加する システムの時刻変更の影響を受けるが、大きく変化することはないし、時間が戻ったりもしない

経過時間をみる指定のようです。 timeout: nanos as u64タイムアウトまでのナノ秒を指定しているようです。

poll_oneoff関数の戻り値の処理は

match (res, event) {
    (
        Ok(1),
        wasi::Event {
            userdata: USERDATA,
            error: wasi::ERRNO_SUCCESS,
            type_: wasi::EVENTTYPE_CLOCK,
            ..
        },
    ) => {}
    _ => panic!("thread::sleep(): unexpected result of poll_oneoff"),
}

パターンマッチで成功か失敗を判定して、失敗したら例外をあげているようです。

おそらくこれと似たような関数を実装して、Kernel#sleepを置き換えるgemを作れば、ruby.wasmでKernel#sleepが使えるようになるはずです。 そうそうruby.wasmはごく最近、gemをwasmバイナリにpackできるようになりました*2。 さて、RubyのRust拡張ってどうやって作ればいいのでしょうか?

*1:前回の記事の後により新しいバージョンを発見しました。

*2:https://github.com/ruby/ruby.wasm/pull/358

ruby.wasmのKernel#sleepをどう実装したものか?

ruby.wasmを使ってブラウザ上でKernel#sleepを呼ぶとエラーが起きます。

<html>
<body>
  <script
    src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-05-a/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    sleep 1
  </script>
</body>
</html>

エラーのスクリーンショット

とりあえずこんなパッチを当てれば動くことはわかっています。

module Kernel
  def sleep(time)
    JS.eval("return new Promise((resolve) => setTimeout(resolve, #{time * 1000}))").await
  end
end

現状Promise#newにコールバックをブロックで渡せないので、JS.evalを使っています。 これは直せると思います。

もう一つ悩みがあります。 JavaScriptsetTimeoutを使っています。 WASIのインターフェースを使ったほうがポータブルになりそうな気がします。

Add a sleep function to the Core · Issue #77 · WebAssembly/WASI · GitHub によると

I'm the current API, the way to implement sleep is to use poll_oneoff, polling for a single __WASI_EVENTTYPE_CLOCK event.

poll_oneoffという関数があるようです。

https://github.com/newpavlov/rust/blob/1e2b711d308be714e6211c125b1a33ac1247f866/src/libstd/sys/wasi/thread.rs#L30-L63をみると、rustではWASI用のThread.sleeppoll_oneoff関数を使って実装しているようです。

これらの情報をみても、ruyb.wasmにどうやって実装したらいいのか皆目見当もつきません。