テレビを耐え抜く小手先技術

この記事は ex-crowdworks Advent Calendar 2018 の21日目です.

CrowdWorksを退職して8ヶ月くらいが経った.

CrowdWorksにいたときはSREをしていたので,テレビ放映があるときには少し覚悟が必要だったんだけど,その話をしようと思う.

あと,ここで書く話は,ただの昔話 であり,現在ではテレビが突然来ても割と耐えられるくらいの状態にはなっていると思う.ので,こんなことをしなくても凌げると期待している.

基本的にCrowdWorksは,クラウドソーシングのサービスなので,ニュースサイトやゲームのサービスほど,普段から大量の接続をさばく必要はない. そのため,そういう人たちにとっては物足りないくらいの対策でしかないと思うことを断っておく.

続きを読む

AWS ECS上にfluentdクラスタを構築し別のVPCからログを送りつける

この記事は scouty Advent Calendar 2018 の17日目です.

fluentdに関して,勢い余って今年こういう発表をした.

speakerdeck.com

完全に酔っていたので勢いだけなのだが,その後マトモなfluentdクラスタができたので書こうと思う.

もう二度とchefで作ろうとは思うまい.

続きを読む

Whalebirdの重かった部分の話

この記事は Mastodon Advent Calendar 2018 の10日目です.

普段はMastodonのデスクトップ向けクライアントWhalebird を開発しています.

https://whalebird.org

あと,Pleromaのインスタンスを運用してます.全然人が少ないし特に何も運営らしきことはしていないんだけど.

https://pleroma.io

というわけで今日はマストドンクライアントの話です. Whalebirdの実装の中でも,結構重かった(いろんな意味で)部分の話をします.

フロントは全部自分で書く必要がある

WhalebirdはElectronベースのMastodonクライアントですが,ただWebページを表示するだけのガワアプリではなく,フロントを全部自前実装してAPIを叩いているアプリです. そのため,前提として,Mastodon本体のフロントエンドの資産はほぼ使えません.

また基本的にAPIが用意されていない機能も使うことができません. そのため,これから書くような「Webの再実装に近いんだけど,全部自分で作らないといけない」状態になってます.

サジェスト

Whalebirdには

のサジェスト機能があります.

これらのサジェストを実装していく上で,考慮点がいくつかありました.

  • 検索は都度APIを叩くのか,起動時にキャッシュするのか
  • 都度APIを叩くとして,キー入力イベント全てに対してAPIコールしていいのか

こういうのに迷ったときにはMastodonのWebを参考にするんですが,Webでは

  • 都度APIを叩いている
  • キー入力イベント全てに対してAPIコールしている

という実装でした. というわけで,Whalebirdでも検索は都度APIを叩くことにしました. ただ,流石にサードパーティー製のアプリケーションで,APIコールしまくるのはどうかと思ったので.キー入力イベントは間引きならがAPIを叩いています.

そのため,基本的にここの部分の速さはインターネットの速度によって制限されます.まぁPC向けなのでそこまで遅い環境で使われることは想定しなくていいかなと思ってます.

キー入力イベントを間引いたとしても,キー入力のたびに

  • 入力文字列のパーサ
  • パースされた結果に応じて検索APIの呼び出し

という処理が裏で走るので,ここはかなりCPUを食う場所です.

2.5.0あたりでやたら入力がもっさりしたバージョンをリリースしましたが,あれの原因はほぼこいつらでした

あそこで,汚かった実装をいろいろとリファクタリングして,現状はこんな感じ.

const textAtCursorMatch = (str, cursorPosition, separators = ['@', '#', ':']) => {
  let word

  let left = str.slice(0, cursorPosition).search(/\S+$/)
  let right = str.slice(cursorPosition).search(/\s/)

  if (right < 0) {
    word = str.slice(left)
  } else {
    word = str.slice(left, right + cursorPosition)
  }

  if (!word || word.trim().length < 3 || separators.indexOf(word[0]) === -1) {
    return [null, null]
  }

  word = word.trim().toLowerCase()

  if (word.length > 0) {
    return [left + 1, word]
  } else {
    return [null, null]
  }
}

export default textAtCursorMatch

ここは本家のソースをだいぶ参考にしました.

github.com

で,現状は普通に使っててストレスないくらいにはなっていますが,やはり多少CPUは使いますね…….

絵文字

もう一つ2.5.0で重くなった要因があります. それが,EmojiMartの絵文字パレットの追加です.

これはMastodon本家に合わせて,EmojiMartのvue版を使っていました.

github.com

ただ,こいつのロードになかなか時間がかかるため, v-show で制御していました.これが重くなる原因で,この重さのコンポーネントを常に裏側に用意しておくと,またメモリを500MB以上食うようになりました.

というわけで最近こいつは v-if で制御するようにしています. そうすると絵文字パレットの表示には時間がかかるんですけどね…….

パーサ

Whalebirdのトゥートにはパーサが仕込まれています. これは,クリック時に以下の判定を行い,それぞれの動作に遷移させるために必要な処理です.

  • クリックされた文字列がアカウント名であれば,アカウントの詳細ページを開く
  • クリックされた文字列がハッシュタグなら,ハッシュタグタイムラインを開く
  • クリックされた文字列が上記に該当しないリンクであれば,OSのデフォルトブラウザで開く

また,これに関してはweb側の実装が一切参考にできません(Reactなので,おそらくパースなんかしなくてもRouterさえちゃんと設定してあれば画面遷移できるので).

というわけで,

export function findAccount (target, parentClass = 'toot') {
  if (target.getAttribute('class') && target.getAttribute('class').includes('u-url')) {
    return parseMastodonAccount(target.href)
  }
  // In Pleroma, link does not have class.
  // So we have to check URL.
  if (target.href && target.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/@[a-zA-Z0-9-_.]+/)) {
    return parseMastodonAccount(target.href)
  }
  // Toot URL of Pleroma does not contain @.
  if (target.href && target.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/users\/[a-zA-Z0-9-_.]+/)) {
    return parsePleromaAccount(target.href)
  }
  if (target.parentNode === undefined || target.parentNode === null) {
    return null
  }
  if (target.parentNode.getAttribute('class') === parentClass) {
    return null
  }
  return findAccount(target.parentNode, parentClass)
}

export function parseMastodonAccount (accountURL) {
  const res = accountURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/(@[a-zA-Z0-9-_.]+)/)
  const domainName = res[1]
  const accountName = res[2]
  return {
    username: accountName,
    acct: `${accountName}@${domainName}`
  }
}

export function parsePleromaAccount (accountURL) {
  const res = accountURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/users\/([a-zA-Z0-9-_.]+)/)
  const domainName = res[1]
  const accountName = res[2]
  return {
    username: `@${accountName}`,
    acct: `@${accountName}@${domainName}`
  }
}

こういうややこしいパーサをいくつか書いています.

リンクを外部ブラウザで開くだけなら,そこまで苦労するものでもないんですが,その手前にハッシュタグとアカウントのパーサを挟もうと思うと,これしかありませんでした.

こいつが重い要因は,要素の親を再帰的に辿るからです. これには理由があって,MastodonAPIが返してくるstatusはhtml構造となっており,

<p>
  <span>
    <a href="https://social.mikutter.hachune.net/@h3_poteto">
      @<span>h3_poteto</span>
    </a>
  </span>
  hogehoge
</p>

こんな感じで,必ずしもクリックした要素が<a> タグになっているわけではないからです.

また,MastodonとPleromaでこのhtml構造が微妙に違う場合があって,それらの差異もこのパーサですべて吸収しています.

まとめ

めちゃくちゃフロントの話になってしまいましたが,そのくらいバックエンドでボトルネックになるところは少ないです. 今のところMastodonAPIはよくできているので,裏側でストリーミング更新したりする部分は,ほとんど重くならずに実装できています.

実は最近の更新で,裏でストリーミングするタイムラインを選べるようになっているんだけど,そこでストリーミング数を増やしても,メモリ消費的にはそこまで上がりません. CPUは少し食うかもしれない,それはWebSocketなので仕方がないのだけれど…….

f:id:h3poteto:20181210231202p:plain

f:id:h3poteto:20181210231207p:plain

というわけで,実装的にも動作的にも重い部分を紹介しました. この辺はできればもっとスマートに高速化していきたいとは思っているので,今後にご期待ください.

RundeckをECS上に構築してGitHubログインできるようにする

この記事はscouty Advent Calendar 2018 の3日目です.

ECSで動かしているサービスのスケジュールジョブが多くなってきた. もともとECS Scheduled Taskを使っていたんだけど,数十個レベルになってくると,これで管理するのはだいぶつらい.

というわけでRundeckを構築したメモ.

続きを読む