ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

ハンズオンWebAssemblyを読んだ

日記、やっぱり書いたほうがいいなとなるきっかけがあったので漠然とやっていく。


この本、バイナリが好きなので読んだ。

雑な感想として:

  • wasm形式自体の基礎やwatの基礎が書かれていてとても良かった
    • 11章でおもむろにwat形式を手書きする
  • emscripten、いいんだけどAPIが癖つよというか…
  • 型がない言語って読みにくいんだな… って思った…
    • 読みやすさが、 C++部分 >>>>> JavaScript部分だなという気づきがあった
    • というかC++ほぼ書いたことないんだけどあんまりそれが障壁にならなかった...型があったから...
    • 第2版が出るならTypeScriptで... ってそうなるとwasm-packみたいにwebpackが出てくるとか、抽象化レイヤが増えて辛いんかな

全体的に隠蔽しているところを、なるべくほぐそうという気持ちが感じられてよかった。

emscriptenの提供するAPIに依存するところはしているが、まあそこはemscriptenを追えばいいのだろう...。自分のやりたいことには近づけた感がある。

次はこれを読んでいるところ。

rustwasm.github.io

wasmよりwebpackの理解に難儀している........これも感想を残したい。

Xの動画を見るときに落とされるファイルを眺める

あまりにも個人の日記を書いていないので、某所にただのメモとしておいてあるやつを再構成したりしてお茶をにごす。


例えばこのツイート... ポスト。

動画を再生しようとするとき、最初に LS8h5YVnzTCKxCEM.m3u8 というファイルにアクセスした。

HLSのマニフェストを眺める

これはHLSのplaylist(マニフェスト)で、中身はこう:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=501854,BANDWIDTH=639498,RESOLUTION=1280x720,CODECS="mp4a.40.2,avc1.640020"
/ext_tw_video/1708859564446429185/pu/pl/avc1/1280x720/_u3fHmjJPHlXgv3W.m3u8?container=fmp4
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=205381,BANDWIDTH=264621,RESOLUTION=640x360,CODECS="mp4a.40.2,avc1.4d001f"
/ext_tw_video/1708859564446429185/pu/pl/avc1/640x360/emjUVd3t94tT8j-1.m3u8?container=fmp4
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=91859,BANDWIDTH=116120,RESOLUTION=480x270,CODECS="mp4a.40.2,avc1.4d001e"
/ext_tw_video/1708859564446429185/pu/pl/avc1/480x270/IhZW9A8k91V6lbX3.m3u8?container=fmp4
  • 再生環境によって動画の品質を選択できるよう、複数のマニフェストを選択させるようになっている。HLS マスターマニフェストというやつ。参考1
  • コーデックは mp4a(AAC) + avc(H.264) のみ、解像度が3種類
  • container=fmp4 とのことだが、ここを例えば mpeg2tsに変えても無視される気がする...

このうち一つ IhZW9A8k91V6lbX3.m3u8 の中身はこう。こちらはメディアマニフェストとか言うらしい。

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="/ext_tw_video/1708859564446429185/pu/vid/avc1/0/0/480x270/BEWYQPCMCXWMJLNs.mp4"
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/0/3000/480x270/SjaCI0mG8RqXyZZw.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/3000/6000/480x270/AEOm4mdlYZWCo9GB.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/6000/9000/480x270/DN4R2KT6gfCdACrh.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/9000/12000/480x270/atCTyDRFdq0I9JWQ.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/12000/15000/480x270/5AwGszExXRlmwlmD.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/15000/18000/480x270/Qq9t_qPDrmuLQNjN.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/18000/21000/480x270/8AviU-V8vo2GuJTe.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/21000/24000/480x270/jSQu_WPDX28DAOtc.m4s
#EXTINF:3.000,
/ext_tw_video/1708859564446429185/pu/vid/avc1/24000/27000/480x270/Ng8Ba10HbqG9oLD9.m4s
#EXTINF:2.600,
/ext_tw_video/1708859564446429185/pu/vid/avc1/27000/29600/480x270/sdqh8euFgH6OtRKe.m4s
#EXT-X-ENDLIST
  • 最初に *.mp4 のファイルにアクセスする必要がある
  • その後、再生時間ごとに(3秒ごとで区切っている模様) *.m4s ファイルをダウンロードして映像・音声のデータを取り出し再生
  • シーク等をしたら、時間を計算していい感じのチャンクを落として再生

Fragmented MP4のファイルたちを眺める

MP4のファイル(MPEG-4 Part 14)はおおまかに言えば Box という単位のデータの区切りの塊で、データ本体の他にメディアのメタデータなんかを含んでおり、Box同士はツリー構造になって格納されている。

気持ち的にはhtmlのタグの入れ子みたいに認識している(実際XMLでダンプするツールなんかもある)。

まず、最初に BEWYQPCMCXWMJLNs.mp4 の構造を眺めてみる。abema/go-mp4 に含まれる mp4tool というコマンドを使っている。

[ftyp] Size=24 MajorBrand="iso5" MinorVersion=512 CompatibleBrands=[{CompatibleBrand="iso6"}, {CompatibleBrand="mp41"}]
[moov] Size=1106
  [mvhd] Size=108 ... (use "-full mvhd" to show all)
  [trak] Size=418
    [tkhd] Size=92 ... (use "-full tkhd" to show all)
    [mdia] Size=318
      [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=0 ModificationTimeV0=0 Timescale=48000 DurationV0=0 Language="und" PreDefined=0
      [hdlr] Size=51 Version=0 Flags=0x000000 PreDefined=0 HandlerType="soun" Name="Twitter-vork muxer"
      [minf] Size=227
        [smhd] Size=16 Version=0 Flags=0x000000 Balance=0
        [dinf] Size=36
          [dref] Size=28 Version=0 Flags=0x000000 EntryCount=1
            [url ] Size=12 Version=0 Flags=0x000001
        [stbl] Size=167
          [stsd] Size=91 Version=0 Flags=0x000000 EntryCount=1
            [mp4a] Size=75 DataReferenceIndex=1 EntryVersion=0 ChannelCount=1 SampleSize=16 PreDefined=0 SampleRate=48000
              [esds] Size=39 ... (use "-full esds" to show all)
          [stts] Size=16 Version=0 Flags=0x000000 EntryCount=0 Entries=[]
          [stsc] Size=16 Version=0 Flags=0x000000 EntryCount=0 Entries=[]
          [stsz] Size=20 Version=0 Flags=0x000000 SampleSize=0 SampleCount=0 EntrySize=[]
          [stco] Size=16 Version=0 Flags=0x000000 EntryCount=0 ChunkOffset=[]
  [trak] Size=500
    [tkhd] Size=92 ... (use "-full tkhd" to show all)
    [mdia] Size=400
      [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=0 ModificationTimeV0=0 Timescale=900000 DurationV0=0 Language="und" PreDefined=0
      [hdlr] Size=51 Version=0 Flags=0x000000 PreDefined=0 HandlerType="vide" Name="Twitter-vork muxer"
      [minf] Size=309
        [vmhd] Size=20 Version=0 Flags=0x000001 Graphicsmode=0 Opcolor=[0, 0, 0]
        [dinf] Size=36
          [dref] Size=28 Version=0 Flags=0x000000 EntryCount=1
            [url ] Size=12 Version=0 Flags=0x000001
        [stbl] Size=245
          [stsd] Size=169 Version=0 Flags=0x000000 EntryCount=1
            [avc1] Size=153 ... (use "-full avc1" to show all)
              [avcC] Size=51 ... (use "-full avcC" to show all)
              [pasp] Size=16 HSpacing=1 VSpacing=1
          [stts] Size=16 Version=0 Flags=0x000000 EntryCount=0 Entries=[]
          [stsc] Size=16 Version=0 Flags=0x000000 EntryCount=0 Entries=[]
          [stsz] Size=20 Version=0 Flags=0x000000 SampleSize=0 SampleCount=0 EntrySize=[]
          [stco] Size=16 Version=0 Flags=0x000000 EntryCount=0 ChunkOffset=[]
  [mvex] Size=72
    [trex] Size=32 Version=0 Flags=0x000000 TrackID=1 DefaultSampleDescriptionIndex=1 DefaultSampleDuration=0 DefaultSampleSize=0 DefaultSampleFlags=0x10000000
    [trex] Size=32 Version=0 Flags=0x000000 TrackID=2 DefaultSampleDescriptionIndex=1 DefaultSampleDuration=0 DefaultSampleSize=0 DefaultSampleFlags=0x10000

最初にこのファイルを参照しないといけないのは、 ftyp というBoxでファイル種類を判定する必要があるのと、 moov というBoxに動画の重要な情報(映像、音声各ストリームのコーデック情報、サンプルレートなど)が含まれるため。一方、ストリーム自体のコンテンツデータは mdat というBoxにあるのだが、それはこのファイルにはなさそう。なのでファイルとしては小さい。

次にセグメントを一つ落として見てみる。 SjaCI0mG8RqXyZZw.m4s:

[styp] Size=24 MajorBrand="iso5" MinorVersion=512 CompatibleBrands=[{CompatibleBrand="iso6"}, {CompatibleBrand="mp41"}]
[moof] Size=2736
  [mfhd] Size=16 Version=0 Flags=0x000000 SequenceNumber=2
  [traf] Size=1192
    [tfhd] Size=16 Version=0 Flags=0x020000 TrackID=1
    [tfdt] Size=20 Version=1 Flags=0x000000 BaseMediaDecodeTimeV1=0
    [trun] Size=1148 ... (use "-full trun" to show all)
  [traf] Size=1520
    [tfhd] Size=16 Version=0 Flags=0x020000 TrackID=2
    [tfdt] Size=20 Version=1 Flags=0x000000 BaseMediaDecodeTimeV1=0
    [trun] Size=1476 ... (use "-full trun" to show all)
[mdat] Size=33789 Data=[...] (use "-full mdat" to show all)

stypmoof などのBoxがあり、セグメント単位のメタデータを持っている。ちなみにメタデータから実際の映像・音声のデータへの参照は trun というBoxに記述されている。

[trun] Size=1148 Version=1 Flags=0x000301 SampleCount=141 DataOffset=2744 Entries=[{SampleDuration=3118 SampleSize=85}, {SampleDuration=1024 SampleSize=85}, {SampleDuration=1024 SampleSize=86}, {SampleDuration=1024 SampleSize=125}, {SampleDuration=1024 SampleSize=135}, ...

DataOffset=2744 というのが、上のデータで言えば moof 2736 bytes + mdatのヘッダ 8 bytes で計算できるとわかる。その後はSampleSize分のデータが順番に並んでいると考えられる。

なお、この辺りのBoxは、FragmentedでないMP4には登場しない。

そしてmdatが小さいとわかる。なので、動画がめちゃ長の場合でも、初回再生や、あるいはシークした場合でも、ちょっとだけ落としてすぐに再生を再開できる仕組みになっている。


言い訳を最後に置いておくと、公開されたファイルにしかアクセスしていないが、若干お行儀の悪い印象を受けるかもしれない。あと僕の理解が間違ってるところがあればこっそり教えてください...。

mruby 3.2 で実行できるmrbバイナリを自作した

手作りの温かみのあるバイナリです。

作りかた

そろそろRustで書くか、と思ってRustで書いた。

bytes というcrateが今回の用途にめちゃくちゃマッチしていた。数値をBig Endianで書き込めればいいので、こういう感じで。

    pub fn write(&self, mut out: impl io::Write) -> Result<(), io::Error> {
        let b = &self.binary_header;
        let mut buf = bytes::BytesMut::new();
        buf.put_slice(&b.ident);
        buf.put_slice(&b.major_version);
        buf.put_slice(&b.minor_version);
        let size_pos = buf.len();
        buf.put_i32(-1);
        buf.put_slice(&b.compiler_name);
        buf.put_slice(&b.compiler_version);

        for section in self.sections.iter() {
            match section.header.ident {
                rite::markers::irep => {
                    let h = &section.header;
                    let i = section.body_irep.as_ref().unwrap();

                    buf.put_slice(&h.ident); //....

                    buf.put_u16(i.pool.len() as u16);
                    for pi in i.pool.iter() {
                        match pi {
                            rite::PoolItem::Str(s) => {
                                buf.put_u8(0); // IREP_TT_STR
                                // ...
                            }
                            _ => {
                                todo!("unsupported")
                            }
                        }
                    }

                    buf.put_u16(i.syms.len() as u16);
                    for s in i.syms.iter() {
                        buf.put_u16(s.name.len() as u16);
                        buf.put_slice(&s.name);
                        buf.put_u8(0);
                    }

                    for _ in i.children.iter() {
                        // ...
                    }
                }
                rite::markers::end => {
                    let h = &section.header;
                    buf.put_slice(&h.ident);
                    buf.put_i32(h.size);
                }
                _ => {
                    unreachable!("unknown section ident")
                }
            }
        }

        // finally write back total binsize
        let size = buf.len();
        buf[size_pos] = ((size >> 24) & 0xff) as u8;
        buf[size_pos + 1] = ((size >> 16) & 0xff) as u8;
        buf[size_pos + 2] = ((size >> 8) & 0xff) as u8;
        buf[size_pos + 3] = (size & 0xff) as u8;

        let freeze = buf.freeze();
        out.write(&freeze[..])?;
        out.flush()?;

        Ok(())
    }

あとはサイズの検知の仕方とかをこなれた感じにするかなー。

できたバイナリ、compiler name が MATZ と違っていてもちゃんと実行できる!

$ xxd /opt/ghq/github.com/udzura/mrbasm/sample.mrb        
00000000: 5249 5445 3033 3030 0000 0056 4d41 534d  RITE0300...VMASM
00000010: 3030 3030 4952 4550 0000 003a 3033 3030  0000IREP...:0300
00000020: 0000 002e 0001 0004 0000 0000 0000 000a  ................
00000030: 5102 002d 0100 0138 0169 0001 0000 0548  Q..-...8.i.....H
00000040: 656c 6c6f 0000 0100 0470 7574 7300 454e  ello.....puts.EN
00000050: 4400 0000 0008                           D.....
$ ./bin/mruby /opt/ghq/github.com/udzura/mrbasm/sample.mrb
Hello

コード

github.com

色々ラフですが。一旦上記に置いた。

リポジトリ名の通り、最終的にはmrubyの命令を直接記述できるアセンブリ言語を作ろうとしている。まだ何も設計できていないが、多分こういう感じ。

!binformat mruby 3.2
!begin irep main:
!rootirep
    LOADI_2       R1      (2)
    MOVE          R2      R1
    LOADI_1       R3      (1)
    GT            R2      R3
    JMPNOT        R2      else1
    STRING        R3      "big"
    SSEND         R2      :puts   1
    JMP           end1
else1:
    STRING        R3      "small"
    SSEND         R2      :puts   1
end1:
    RETURN        R2
    STOP
!end

!begin irep sub:
    LOADI_2       R1      (2)
    MOVE          R2      R1
    ...
!end

ただまあ、このasmは正直やろうとしていることに必要だから作っていて、ゴールでもなんでもない。作りたいのはもうちょっと別のもの。

ということでRubyKaigi お疲れ様です。みなさんと沖縄で会えるといいっすね。

mruby 3.2.0 のバイナリフォーマット

なんとなくバイナリを解析してえ〜と思ったので、mruby 3.2.0 (最新stable?)の .mrb ファイルのフォーマットを眺めることにした。

Rustでパースしました!だとかっこいい、ナウだなと思ったけれど、動的型育ちな自分をどうしても甘やかしてしまい、 Rubyunpack を軸に解析した。いやほんと、今やRubyで一番使うメソッドでは。

続きを読む

自作 LSM-Tree その1

Log-Structured Merge Tree というデータ構造があって、データ指向アプリケーションデザインを読んでいるとかなり最初の方に出てくる。Wikipediaの記事の通りLevelDBを始めきょうびのさまざまなデータベース製品で使われている。

特徴はめちゃくちゃざっくり*1

  • 追記型のログを使うことで書き込みの性能を保つ
  • インデックスはキー名とログ内のオフセットzを持っておき、読み出しも速度を出す
  • ログを SSTable というデータ構造に退避することで、インデックスはある程度疎にしつつ(めちゃくちゃキーが多くなってインデックスの時点でデカくなるなどを防ぐ)速度を保ってアクセスできる
  • 必要に応じてマージなどをしてインデックスは最新のデータだけに、小さくする

今回お仕事とか色々でLSMを使うため、コードの理解を助けるためにまずは自分で実装しようと思ったので、久しぶりの連作ブログ記事を始めた。

自分で実装と言いつつ細かいところをオミットしていたり、勘違いもあると思うので、優しくご指摘いただければ...。

そんな感じです。

やること

LSM-Tree indexを持つログ追記型のファイルベースDBを作る。

細かいチューニングは置いておく。

言語はGoです。

*1:この時点で誤解があったら指摘してください...

続きを読む