Stream#pipeの破棄イベントの伝播
あっさりハマってメモリリークさせまくったので報告。
さっそく本題。まず図を用意。
Readable Stream -(pipe)-> Transform Stream -(pipe)-> Writable Stream
みなさんご存知のデータの流れ。しかし、問題は破棄イベントの伝播方向。
Readable Stream が閉じた場合
- Readable Stream が閉じる
- Transform Stream が閉じる
- Writable Stream が閉じる
=> 全部閉じる
よいですね。
Writable Stream が閉じた場合
- Writable Stream が閉じる
- おしまい
_人人人人人人人人人人人人人人人人_ > Writable Stream しか閉じない <  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
=> 源流側が破棄されない
まとめ
正直、知らんかった(バカ)。
"Stream"について考えていたらConduitを知った
Iteratee ってちょっと古かった感じですかそうですか。今更騒ぎ立ててごめんなさい。
Iteratee わけわかんねーな、run ってなんだよとか思っていたら、Conduit を見つけた。push 型から pull 型へ先祖返り、と。なんだか Stream(1) から Stream2 への移行みたいなことがこっちでも起きてるんだな。
さて、Iteratee について疑問だったことといえば、Enumeratee 同士の結合がなかったことだけど、Conduit では Conduit 同士を結合できるっぽい。ちなみに、流れ的にはこんな感じ。
Source -pull-> Conduit -push-> Sink
Conduit が左から引っ張りだして右に突っ込む感じのよう。まだよくわからん(コードも読んでないから)雰囲気だけど。
ちなみに結合則(っていうのかな?)をまとめるとこんな感じ。(相変わらずのオレオレ記法でごめんなさい)
Source $= Conduit := Source Conduit =$ Sink := Sink Conduit =$= Conduit := Conduit
BNF を書いているみたいでとても素敵だ。Conduit を Source と結合しても Sink と結合してもいいあたり、とても美しい。やばい。
詳しくは とか Data.Conduit とか読んで俺も今から勉強します。
"Stream"について考えた(それとIteratee)
タイトルは釣り。どっちかというと Iteratee がメイン。(だってみんな Iteratee とか見ても記事開かないでしょ)
1週間くらい Stream について考え続けて、やっぱりわからなくて。でも、こんな基本的な概念、誰かが既に考えつくしてるに違いない、と思ってググってみたら、あった。そして、またお前か、Haskell。
Iteratee
ほとんど Stream。モナドとかオートマトンとか言い始めるときりがないので演算子とその働きだけ大雑把に眺めてみた。
Gist: Iteratee とその演算子の働きとか。すっごく大雑把で、記号も適当ですが雰囲気だけ。
================================================================== Enumerator -> Enumeratee -> Iteratee ================================================================== Iteratee >>== Iteratee := Iteratee ------------------------------------------------------------------ Enumerator $$ Iteratee := Iteratee ------------------------------------------------------------------ Enumeratee =$ Iteratee := Iteratee ------------------------------------------------------------------ Enumerator $= Enumeratee := Enumerator ------------------------------------------------------------------ Enumerator >==> Enumerator := Enumerator ------------------------------------------------------------------
Stream に置き換えると、Enumerator が Readable、Enumeratee が Transform、Iteratee が Writable、という感じ。ちょっと違うけど、そんな感じ。
Stream と違うのは、Enumerator 同士の結合と、Iteratee 同士の結合。これが型と相まって地味に便利っぽい。
ということで JavaScript で Iteratee を実装してみてます。どうなることやら。
続: Stream2のモヤモヤ
追記で書くほどの分量でもなくなってきたので別にしました。
まず結論からいうと、
ドキュメントが追いついてない
だけだったようです。このコミット streams: Support objects other than Buffers · 444bbd4 · joyent/node · GitHub での変更が、ドキュメントに反映されてないようです。ドキュメントは the entire buffer とか言ってるけど、ソースコード的には objectMode のときには size 1 なのね……
If we are in "objectMode" mode then howMuchToRead will
always return 1, state.length will always have 1 appended
to it when there is a new item and fromList always takes
the first value from the list.
俺の半日を返せと。
本題
さて、謎の原因がわかったところで本題。
現状の stream2 は buffer, string, object を chunk としてサポートしている。しかし、他の2つとくらべて object は特殊。どれくらい特殊か、下の表にまとめてみた。
length プロパティ | 連結の可否 | 任意の型のデータの格納の可否 | |
---|---|---|---|
string | ある | concat メソッドで可能 | 不可 |
buffer | ある | concat メソッドで可能 | 不可 |
object | 意味ない | object の連結? なにそれ | 可能 |
圧倒的に浮いてる。同列に並べちゃいけない。それに対して array はどうか。
length プロパティ | 連結の可否 | 任意の型のデータの格納の可否 | |
---|---|---|---|
string | ある | concat メソッドで可能 | 不可 |
buffer | ある | concat メソッドで可能 | 不可 |
array | ある | concat メソッドで可能 | 可能 |
これでしょ。サポートするなら。
ストリームの chunk は joinable で size(length) を持つべきだと思うんです。どこで切ってもよく、どこでつなげてもいい、そういうデータが chunk であるべきだと思うんですよ。
結論
object のサポートなんてやめて、array をサポートすべき。
とはいえ
「過去のモジュールでは object を chunk として流すものも多い。互換性はどうするんだ」とかいう反論があるかもしれない。でも個人的には stream2 は stream と決別して進化して欲しいし「歴史的経緯」なんてもので汚れてほしくない。stream1 は生かしつつ、stream2 が別に生まれるくらいでいいんじゃないかとも思う。
引数なし read() が返すデータのサイズについて
@KOBA789 ずっと気になってるのが、ブログにあった「引数なしの read() を一度呼べば全てのデータが吸い出せるという前提」なんだよね。これがなんか stream の考え方とずれている感じが引っかかってる。そこも言及されたブログだとより良いと思います。
2013-02-11 14:37:24 via Echofon to @KOBA789
とか言われたのでちょっと考えてみたけれど、適切なバッファサイズとか流量とか、まだよくわかってない……。なのでもう少し流量制限について調べて、触ってみて、なにかわかったらまた記事書こうと思う。
Stream2のモヤモヤ
とりあえず、分かったことだけ。
正しいことを書いてるとは限らないよ!! むしろ昨日はじめて stream2 に触ったからツッコミ待ちだよ!! ドキュメントもサンプルコードも少なくて困ってるよ!! 助けて!!
3つのモード
readable stream には stringMode と objectMode、それと名前はついてないけどいわゆる bufferMode がある。で、stringMode と bufferMode はほぼ同一なのでいいとして、問題は objectMode。
ReadableStream#read([size])の挙動
read() は引数 size が省略された場合、キューの全てを返すことが期待される*1。
しかし、objectMode の場合、キューの中から1つだけ返されるだけである。引数なしの read() を一度呼べば全てのデータが吸い出せるという前提でコードを書くとこの仕様にぶち殺される気がする。返り値が null になるまで読めという指摘もあるかもしれないが、キューに null を突っ込むこともできる。あちゃー。
個人的には、read(2) とかで [{obj: 1}, {obj: 2}] みたいになってほしい。つまり、キューから size ぶんだけ配列で返してほしい。そうすれば read() のときにも [{obj: 1}, {obj: 2}, {obj: 3}] になるし、中身に null が入っていても [null, {obj: 1}] となるから区別できる。
この意見の妥当性をもう一度自分で確かめられたら本家にアイデア投げてみようかなーと思う。
追記1
ReadableStream のことしか考えてなかった。pipe するときに WritableStream#write に配列が投げられたらアレゲだった。でも、結合不可なデータをストリームに流すのってどうかと思うから、オブジェクトを write するときは write([obj]) にしろ、っていうのも悪くないんじゃないかと思った。そうすれば複数チャンクを一度に書き込めるし。
まだまだ考えてみようと思う。
追記2
Twitter 上で id:Jxck さんにいろいろ指摘をいただきました。現状の Stream2 に対する理解が深まりました。ありがとうございます!
ログをだらだら貼り付けておくので、まとめだけ読みたい人は下へどうぞ。
実際、特に違和感は無いなぁ。chunk としてオブジェクトが一つづつ返ってくることが保証されていれば、むしろその方がいいくらいな気も。
@Jxck_ そうすると、object として null を突っ込んだ時の挙動が……
2013-02-11 13:17:28 via YoruFukurou to @Jxck_
@KOBA789 あれ? null の時って objectMode になる?URL
2013-02-11 13:18:47 via Echofon to @KOBA789
@Jxck_ いや、一度 objectMode になったあとで、データとして null が来るとアウトだなー、と。null なんて使うな、という意見も一理ありますが。
2013-02-11 13:19:39 via YoruFukurou to @Jxck_
@KOBA789 データとしての null は object chunk では無いと考えると、それもそれで妥当な気もするなぁ。だって chunk に対して処理できることが無いのは変わらないんだし。
2013-02-11 13:21:19 via Echofon to @KOBA789
@Jxck_ なるほど。そうすると引数なしの read がキューの全てを返すという仕様を覆すのが手っ取り早いことになりますかね?
2013-02-11 13:22:20 via YoruFukurou to @Jxck_
@KOBA789 例えば read() でキューを全ぶ返すのを覆すとして、 buffer の場合何を返せば妥当かな?
2013-02-11 13:25:29 via Echofon to @KOBA789
@Jxck_ あー、なるほど。段々わかってきました。非 objectMode なら結合されてどこまでも 1 chunk である、みたいな考え方ですかね。
2013-02-11 13:26:41 via YoruFukurou to @Jxck_
@KOBA789 そう。つまり、 read() で全部返すのを覆すなら、同時に 「buffer における chunk とは n byte である」という defaultChunkSize が必要になると思う。そのサイズはどう決めるんだって話になるかなぁ。
2013-02-11 13:29:13 via Echofon to @KOBA789
@Jxck_ 現状の stream2 に対する考え方については理解出来ました。ありがとうございます。ただ、理解した上で、個人的にまだその仕様に反論みたいなものがあるので、再度ブログにまとめようと思います。
2013-02-11 13:36:52 via YoruFukurou to @Jxck_
@KOBA789 まあ、俺も探り探りだけど、コメント読むとそんな感じなのかなと。それ以外、やりたいことは _read() の override でやれという感じもするかも。ともあれ俺も参考になりました。 blog 期待 !
2013-02-11 13:38:53 via Echofon to @KOBA789
以下、自分の勝手な解釈。必然的に間違いを含むはず。(てか正解があるなら今すぐ欲しい感)
「書き込む」というとややこしいので、ReadableStream のキューにデータを追加することを「キューに吸い込む」と呼ぶことにする(便宜上)。
まず、ReadableStream には大きく2つのモードがある。それが、objectMode と非 objectMode。キューにデータを吸い込む際、そのデータが string, buffer, null, undefined のいずれでもない場合、自動的に objectMode になる。一度 objectMode になると二度と非 objectMode にはならない。ストリームは object に「汚染」される。
……と、ここまで書きかけて、chunk と呼んでいたものは buffer で、chunk と buffer には区別があるらしいことがわかってしまったので、もう一度まとめ直したい。
あと、コミットログ追っかけてわかったこともあるので、全部まとめ直します、はい……
追記3
リンク張ってなかったけど、続き書きました→ 続: Stream2のモヤモヤ - Write and Run
Object.observeに関して(あと bind-unit とか)
先日
血迷って どうしてもObject.observeを使いたい場合 - Write and Run なんて記事を書きましたが、再びよくソースを呼んでみたら、setTimeout で polling してました。全然クールじゃない。まぁ、そんなもんだろうとは思っていたけれど。(もちろん、ネイティブ実装ではアクセスフックしてるので高速です。早く標準になれ)
そういえば先日、KOBA789/bind-unit · GitHub なんてものを作りました。こいつはプロパティの new, delete イベントこそ補足できませんが、update ならちゃんと補足できます。ついでに Array を監視する機能も追加予定なので、Object.observe の研究よりそっちを先にやろうかなーと思ってます。というか、今思えば EventEmitter のインターフェース実装する必要なかった気がする。(おかげでいろいろ面倒になってる)
日本科学未来館へ行ってきた
こんばんは。KOBA789 です。今日は「日本科学未来館」へ行って来ました。小学校の遠足で行ったきりなのでだいぶ久しぶりです。
やはり建築の基本に抗って下部が狭く、上部が広くなっている形状の建物は面白くていいですね。内部の展示ももちろんですが、外から眺めていても飽きません。建物自体が1つの展示物のようです。
「インターネット物理モデル」
これ。マジ。
いや、ほんとすごいんですって。メカで(擬似)IP パケットをルーティングするんですよ。しかも、黒と白のボールを用いて 1bit を表す方式で(擬似)IP パケットをまさに手作業で構築して、送信できるんです。ちゃんとコード表も横に置いてあります。そのデータ(白と黒のボール群)はそのままコロコロとレールを転がっていくので、コードを覚えてしまえば流れているパケットを目視で読めます(最後の方は目が慣れてきてだいぶ読めるようになってました)。
また行きたい
合計すると2時間くらいその展示を見ていた気がしますが、まだまだ足らないのでまた行きたいです。日本科学未来館には年間パスポートもあって、1200円(新規・再入会時)と格安です(継続は1000円なので更に安い)。18才以下なら2ヶ月に1度、大人なら半年に一度以上行くと元が取れる計算なので、頻繁にパケットを眺めたい人にはおすすめです。
あと、メカニカルなルーターの構造に関して、光学センサーで先頭 1byte のアドレスを読んでることだけはわかったのですが、上流のルーターに流す部分とかあまりよくわからなかったので、今度行った時にスタッフを質問攻めにしたいところです。
以上、とってもおすすめな日本科学未来館ですので、パケットを眺める趣味がある人もそうでない人も、是非足を運んでみてください。