午前起床は何時起床なのか

導入

しばしば Twitter では「午前起床」というツイートが観測される。しかし、それらは午前ではなく午後にツイートされることも少なくない。

そこから「午前起床とは何時起床なのか」という疑問が生じるのは極めて自然なことであるが、これを実際に調査した文献は筆者の観測するところ未だ無いように思われる。

したがって本エントリでは、Twitter から「午前起床」ツイートの実データを収集し、分析・調査するものである。

データセット

Twitter API 経由で「午前起床」を含むツイートを検索し、12/4 ~ 12/12 における 139 ツイートを収集した*1

    query = f'"午前起床" since:{since} until:{until} -RT -filter:replies'

    # https://developer.twitter.com/en/docs/twitter-api/v1/tweets/search/api-reference/get-search-tweets
    result = api.search(
        q=query,
        lang="ja",
        tweet_mode="extended",
        result_type="recent",
        count=100,  # up to a maximum of 100
    )

データを眺めてみたところ、bot や Q&A サービスの自動ツイートが散見された。

これらをデータセットから除去するため、ルールベースの前処理により最終的に 126 ツイートを得た。

分析結果

ツイート時刻(N 時台)とツイート件数でヒストグラムを描いた図を以下に示す。

ツイート時刻と件数のヒストグラム
ツイート時刻と件数のヒストグラム

考察

上図より、中央値とピークは午前 11 時台であり、午前 10 時台 ~ 午後 12 時台 の「午前起床」が全体の約半数を占めることが分かる。

実際のツイートを眺めてみると、午前 11 時台の「午前起床」は滑り込み成功を喜ぶツイートが、午後 12 時台の「午前起床」は失敗を悔やむツイートが多く見られた。

ヒストグラムから気になるのが裾の端にあたる部分であるが、具体的にツイートを見てみると、午前深夜帯の「論理明日は午前起床するぞ」という宣言系ツイートや、夕方以降の「今日は午前起床で疲れた」という報告系ツイートが含まれていた。これらのデータ除去は今後の課題とする。

午前深夜帯や午後深夜帯におけるその他の「午前起床」には、ツイッターに住まう「本物」が濃縮されており、本エントリでは割愛する。

また「午前起床」のツイート数を screen name 単位で数えてみたところ、一部のユーザーが頻繁に「午前起床」していることも見て取れた(実際のツイート時刻についてはお察し願いたい)。

ツイート数の多いユーザーとツイート数
ツイート数の多いユーザーとツイート数

まとめ

日本時間 13:00 までに起床できたならば、あなたは日本の Twitter ユーザーのうち上位 75% の午前体内時計で生活できているものと結論付けられよう。

しかし、個々人が独自のタイムゾーンを有している場合はその限りでない。

皆さん良い起床を。

*1:無料枠では 1 週間分のデータしかアクセスできないため。有料プランを申し込む、または継続的にクロールすることでデータセットをより増やせると考えられる。

sync.Pool と unsafe.Pointer は混ぜるな危険

Go で書いた API サーバーでなかなか不思議なバグに遭遇したのでメモ。

バグの発生状況をできるだけ簡単化して記述すると以下の通り。

func handleFoo(res http.ResponseWriter, req *http.Request) {
    var bytes []byte = fetchBytes() // ライブラリ使用

    foo := *(*string)(unsafe.Pointer(&bytes))

    callExternalAPI(foo) // 外部 API 呼び出し

    renderJSON(res, foo) // foo を JSON に整形して返す
}

この API レスポンスに含まれる foo の内容が確率的に壊れる。

バグの起きたレスポンスを見るに、何かメモリが壊されているような雰囲気は察したので、unsafe.Pointer 周りが怪しそうな予想を立てつつも、元となる []byte はリクエストの goroutine 毎に独立なはずだしなあ、と悩む。

当然ながら、外部 API を呼ぶ際に foo が壊されているのかもしれないと疑うも、

  1. 外部 API を呼ぶ前にレスポンスを返すとバグは起きない
  2. 外部 API を呼ぶとバグが起きる
  3. foo の代わりに任意の string を与えてもバグが起きる

となり、さながら ハイゼンバグ かとさらに悩む。

さらに調査を進めると、ライブラリを使用している fetchBytes() の中身もさらに怪しくなってきた。これも説明のために簡単化して書くと、

var pool sync.Pool

func fetchBytes() []byte {
    parser := pool.Get().(*Parser) // 何らかのパーサー
    defer pool.Put(parser)

    parser.Parse() // 何らかのデータをパースする

    return parser.GetBytes() // []byte を返す
}

事実、この fetchBytes() は他の API でも使われており、sync.Pool ということは一度アロケートしたメモリ領域を可能な限り使い回す意図だから、ここに何かヒントがあるのでは……? と思い、Parser の中身も調べると、

type Parser {
    cache []byte
}

func (p *Parser) Parse() {
    var data string = someString()

    p.cache = append(p.cache[:0], data...)
}

func (p *Parser) GetBytes() []byte {
    // 説明のため簡略化
    return p.cache
}

これで今回のバグの原因が明らかに。まとめると次の通り。

  1. 問題の API が叩かれ、Parser の保持したメモリ領域上(cache)にデータが読み込まれる
  2. unsafe.Pointer を使うため、foo の string は Parser の保持するメモリ領域上を指す
  3. 外部 API を呼んでレスポンスが返ってくるまでに、一定の待ち時間が発生する
  4. この待ち時間に、同じく fetchBytes() を呼ぶ他の API が叩かれる(別スレッド・別 goroutine)
  5. sync.Pool から取り出した Parser が 1. と同じものだった(既に pool.Put() 済みだった)場合
  6. Parsercachep.cache[:0] でクリアされるだけなので、同じメモリ領域上に別のデータが展開される
  7. こうして最初のスレッドで処理されていた foo は意図しないデータを指してしまう

もちろん、実際のコードはさらに複雑で、fetchBytes() とお茶を濁していたライブラリは valyala/fastjson だったりする。

今回の場合、そこまで大きいデータを扱っているわけでもなく、[]bytestring 変換が何回も走るわけでもないので、単純に string(bytes) で明示的なコピーをするよう修正して事なきを得た。

過度なパフォーマンスチューニングは YAGNI だと再認識。

PR

私が Lead Engineer を務める Qufooit では、Go・k8s を中心にサーバサイドエンジニアを募集しています。私たちと一緒に世界へ通用するサービスを開発しませんか?

www.wantedly.com

Bolt 製 Slack Bot で app_mention イベントに反応させる

Bolt のチュートリアル には app.message() しか取り上げられていないが、app.message() だと app_mention イベントに反応させることができない。

Bot 宛のメンションにだけ反応させたいとか、スコープを app_mentions:read だけに絞りたいときに、わざわざ message イベントまで subscribe する必要があって不便。

f:id:hashedhyphen:20200505011721p:plain
Slack Bot が subscribe するイベントの設定画面

実際にはこんな感じに書く必要がある。

giste25c64094a3738d2efb42459f26fcd06

解説

app.message() やその前後のソースを眺めてみると、実は message イベントしか subscribe されていないこと、本質的には app.event("message", matchMessage(pattern)) の alias だということが分かる。

https://github.com/slackapi/bolt/blob/455bf5849708c8cea4f0683ca45d900c29f97535/src/App.ts#L312-L327

matchMessage(pattern) もソースを追いかけてみると、pattern に合致した post のみ通過させるフィルタを作るヘルパー関数だと分かる。ただ実際に import して使ってみると分かるが、何故か app_mention イベントに転用できない(微妙に型が違う)。

なので、上記のように matchMessage() と似たようなフィルタを自前で書きつつ、app.event("app_mention") でリスナーを登録してあげればちゃんとメンションを聞ける。

めでたい。

余談

とはいえ書き味が少しだるいので、もうちょっといい感じに書ける API 欲しいなーと思って Issue だけ立ててみた。同意得られたら PR 送ろうと思う。

github.com

2020-05-28 追記:PR が master にマージされた。

github.com