mizdra's blog

ぽよぐらみんぐ

VSCode で tsserver や ESLint が通知するエラーがおかしくなった時にやること

TypeScript を書いていると、tsconfig.json を変更したのに何故かその設定が tsserver に読み込まれないとか、vscode-eslint に変更が通知されずに古い型情報を使って lint し続けているとか、そういうことが多々ある。大体 VSCode を再起動すれば解決するのだけど、手間だし遅いし計算資源が勿体ない…

という訳でここでは僕が普段使っている再起動以外の workaround をいくつか紹介する。

F1 > Developer: Restart extension host

日本語だと「開発者: 拡張機能のホストを再起動」という項目。これを実行するとユーザがインストールした拡張機能や VSCode に bundle されている拡張機能 *1 を再起動することができる。全拡張機能の再起動するのでそこそこコストが掛かるけど、殆どのケースで問題が解決するし、VSCode 内で完結するので再起動よりはずっとお手軽。

F1 > TypeScript: Restart TS server

日本語だと「TypeScript: TS サーバーを再起動」。tsserver を再起動できる。VSCode 上に表示される TypeScript のエラーがおかしい時は、大体これを試せば解決する。1つ注意すべき点があって、TypeScript ファイルをカレントタブで開いていないと TypeScript: Restart TS server という項目が出てこない。もし F1 のメニューに TypeScript: Restart TS server が出てこなくて困っていたら、ちゃんとカレントタブで TypeScript ファイルを開いているか、をチェックすると良い。

tsconfig.json をカレントタブで開いて上書き保存する

tsconfig.json を保存すると tsserver が tsconfig.json を再読み込みしてくれるようになってるっぽくて、これだけで解決することもある。

.eslintrc をカレントタブで開いて上書き保存する

tsconfig.json の vscode-eslint 版。

*1:Git の拡張機能や tsserver client など

global install したパッケージを引き継ぎつつ Node.js のバージョンを上げる

時々 nodenv やら nodebrew やらで Node.js のバージョンを上げているのだけど、素朴にバージョンを上げるだけだと npm で global install したパッケージが引き継がれない。その結果、新しいバージョンで手作業で global install し直す作業を強いられることになる。

そうした作業を回避できるよう、大体どのパッケージマネージャーにも global install したパッケージを新しいバージョンの Node.js に移行するコマンドが用意されている。

が、そう高頻度でアップデートしないので、どういうコマンドだったのか忘れて毎回調べ直している… という訳で備忘録という形でいつも叩いているワンライナーを以下に貼っておく。今の所 nvm は使ってないので省略している。

nodenv

NEW_VERSION=15.11.0; OLD_VERSION=$(node -v | tr -d v); nodenv install $NEW_VERSION && nodenv migrate $OLD_VERSION $NEW_VERSION

nodebrew

NEW_VERSION=v15.11.0; OLD_VERSION=$(node -v); nodebrew install $NEW_VERSION && nodebrew use $NEW_VERSION && nodebrew migrate-package $OLD_VERSION

rule ごとに高速に eslint --fix できるツールを作った

大量のエラーに対して rule ごとに高速に eslint --fix できるツール 「eslint-interactive」 を作ったので、その紹介です。

動機

ESLint のデフォルトの出力はエラーの発生源や修正のためのヒントなど、開発者に役立つ多くの情報を含みます。これは多く場合機能しますが、膨大な量のエラーが報告される状況ではあまり機能しません (例えばプロジェクトに ESLint を導入する時や、プロジェクトの .eslintrc に大幅な変更を加える時など)。そうした状況では ESLint の出力が膨大になってしまい、開発者による出力の分析が困難になってしまいます。また、多くの種類のエラーがごちゃまぜになって出力されているため、エラーを修正するのも困難です。

そのため、このような多くのエラー発生する状況では、以下の 2 つの事柄が重要であると考えています。

  • 全てのエラーをまとめた概要を表示し、簡単に全体を把握できること
    • エラーごとの詳細を表示しても、かえって開発者を混乱させてしまいます
  • 大量のエラーを効率よく修正する手段が提供されていること
    • eslint --fix はエラーを効率よく修正する優れた手段の 1 つですが、一括で全ての rule のエラーを auto-fix してしまいます
    • rule によってはコードの挙動に影響を与える auto-fix を行うため、auto-fix は慎重に実施する必要があります
    • そのため eslint --fix よりも小さい単位で auto-fix できる手段が提供されていることが望ましいです

eslint-interactive の紹介

そこで eslint-interactive という ESLint を wrap したツールを作成しました。このツールでは全てのエラーを rule ごとにグルーピングし、rule あたりのエラー数を整形して出力します。rule ごとの warningerror の内訳に加え、fixable なエラーの数など、開発者がエラーを修正するためのヒントも出力されます。また、いくつかの rule を指定して、生の ESLint のエラーメッセージを表示したり、auto-fix を実行することができます。

github.com

こんな感じに rule ごとのエラー総数を眺めつつ、rule ごとに auto-fix していけます。

www.youtube.com

インストール方法や使い方は README を見てもらえれば分かると思うので、先程貼ったリポジトリから辿って下さい。

eslint-nibble との違い

eslint-interactive とよく似たツールに eslint-nibble があります。どちらも同じ問題を解決するツールですが、eslint-interactive には eslint-nibble にないいくつかの機能があります。例えばeslint-interactive はruleごとのfixableなエラー数を出力しますが、eslint-nibble は出力しません。また、eslint-interactive はruleごとのauto-fixのサイクルを高速で回すための様々な工夫 *1 がありますが、eslint-nibble は一度 auto-fix を行うと毎回プロセスが終了するため、eslint-interactive ほど高速にauto-fixのサイクルを回すことはできません。

どちらも同じ問題を解決できるツールではあるのですが、これらの機能が自分にとって非常に重要だったので、新規にeslint-interactive として実装してみました。

おわりに

元々とあるプロジェクトの.eslintrc を大改修する機会があって、その作業を円滑に進めるために用意したツールでした。実際に eslint-interactive を何度か使って作業してみていますが、最初に semi などコードの挙動を変えない rule の auto-fix を適用 => 次に import/order などコードの挙動を変えうる rule の auto-fix を適用、といったように段階的に安全に作業を進めることができて、快適に過ごせてます。皆さんも同じような状況に遭遇したらぜひ eslint-interactive を試してみて下さい。

*1:Lint結果をキャッシュする機能や、プロセスを維持したまま連続で auto-fix できる機能が備わってます。

ブラウザにおけるメモリリークを解決するために読んでおけると良い資料

最近趣味や仕事の Web アプリケーションでメモリリークに遭遇して、頑張ってメモリリークの原因を突き止めて修正する、ということがあった。その過程でメモリリークについて色々調べて知見が溜まったので、学習資料の紹介という形でアウトプットしてみる *1

前置き

  • 紹介する記事がかなり偏っていることに注意
    • 冒頭で触れたメモリリークを解決するために読んだ記事をまとめただけなので、内容にそれなりの偏りがある
    • 例えば id:mizdra が遭遇したメモリリークは全てブラウザ上で発生していたものだったので、これから紹介する内容も主にブラウザにおけるメモリリークに焦点を当てたものになる
  • GC がどうメモリをどう解放しているか、何故メモリリークが発生するのかは全てカット
    • 調べれば色々な記事が出てくるので、必要に応じて読んでください

基本的な知識を抑える

  • まずメモリリークとメモリ撹拌の違いを学ぼう
    • どちらも同じメモリに起因する問題だけど、アプリケーションの性質によって深刻度が異なったり、対処方法も変わるのでちゃんと区別しておけると良い
    • こういうのは世の中の事例を見ると分かりやすい
    • https://www.html5rocks.com/ja/tutorials/memory/effectivemanagement/
      • Gmail におけるメモリリークの事例
      • 内容は結構古いけど、書いてあることのほとんどは現代でも通用する
    • https://www.html5rocks.com/ja/tutorials/speed/static-mem-pools/
      • メモリ撹拌 (メモリリークとは違う!) の事例
      • メモリ撹拌自体は GC が実行されればメモリ使用量は元に戻る
      • 頻繁に GC が発生することで GC によるメインスレッドの停止が発生するので、常に 60fps 維持したいアプリケーション(FPS ゲームとか)などでは問題になる
      • 「メモリ撹拌にはオブジェクトプールが有効だよ」みたいな話題も書かれている
        • 逆にここからメモリリークに対してはオブジェクトプールはそれほど有効ではないことも分かるはず
  • Chrome devtools を使ったメモリ問題の特定方法を学ぼう

ここまで読めば、一通りメモリの問題に対処できる力がつくはず。

その他メモリにまつわる技術資料

  • https://v8.dev/features/weak-references
    • WeakRef / FinalizationRegistry を使うと特定のメモリリークを解決できるケースがあるよ、という話
  • https://web.dev/monitor-total-page-memory-usage/
    • メモリ使用量を測定する API にperformance.memoryというものがあるけど、API 実行直前に GC が発生していたか、そうでないかによって測定結果が大きく変わって使い物にならない
      • GC 発生直後ならメモリ使用量が少なくなり、発生から十分時間が経過している場合はゴミが残っているのでメモリ使用量が大きくなってしまうはず
    • それを受け、この記事ではperformance.memoryの代わりに事前に GC を実行してからメモリ使用量を測定してくれるperformance.measureMemoryを使いましょう、という話が紹介されている
  • GC の挙動をもっと知りたい場合
  • GC の停止時間がアプリケーションの価値に直結する場合 (FPS ゲームなど) は GC の挙動も知っておけると良い
  • V8 チームが出している記事を読むのがオススメ
    • まず GC の基礎を説明して、それから Chrome 固有の実装の詳細について説明するというスタイルになっていて、比較的読みやすい
    • Chrome が現代のモダンブラウザの中では一番イケているはずなので、とりあえず Chrome だけ抑えておければ良い
    • いわゆる「リファレンス実装」として利用する
  • https://v8.dev/blog/trash-talk
  • https://v8.dev/blog/concurrent-marking
    • Concurrent Marking に関する詳細
  • https://v8.dev/blog/high-performance-cpp-gc
    • Blink(Chrome のレンダリングエンジン)で利用されている GC「Oilpan」の仕組みの話
    • DOM などは Orinoco ではなく Oilpan によって管理されるので、こっちも読んでおけると良いはず

*1:元々社内向けに書いた資料で、それを公開できる形に調整した。

スナップショットテストの向き不向きについて考えてみる

ふとスナップショットテストってなんだろう、どういう場面で向いていて、どういう場面には向いていないんだろうと考える機会があって色々調べてました。丁寧な記事にしようとしたのですが、上手くまとまらなくて挫折してしまった… とはいえこのまま手元に置き続けておくのも勿体ないので、下書き段階のものを公開して供養します。

スナップショットテストとは

スナップショットテストとは、あるプログラムの出力を以前の出力と比較し、両者に差分があるかをテストする手法のことです。予め以前のバージョンのプログラムの出力 (スナップショット) のどこかに保存しておき、新しいバージョンのプログラムの出力と比較し、差分があったら fail させます。これにより、プログラムの出力内容が予期せぬうちに変わってしまっていた場合に気づくことができます。

例: React コンポーネントのテストへの適用

代表的な利用例が Jest を使った React コンポーネントのテストです。

// Jestのドキュメントから引用
// from: https://deltice.github.io/jest/docs/ja/snapshot-testing.html
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('renders correctly', () => {
  const tree = renderer.create(<Link page="http://www.facebook.com">Facebook</Link>).toJSON();
  expect(tree).toMatchSnapshot();
});

この例では、<Link>コンポーネントの render 結果をシリアライズし (const tree = renderer.create(...))、その結果をスナップショットとして保存・比較するよう Jest に指示しています (expect(tree).toMatchSnapshot())。このテストを初めて実行すると、ファイルシステムに以下のようなスナップショットファイルが作成されます。

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

2 回目以降テストを実行する際は、このスナップショットファイルとの差分が計算されます。もし<Link>コンポーネントの実装にミスがあり、予期せぬうちに render 結果が変わってしまった場合はスナップショットに差分が発生し、テストが fail するようになります。

スナップショットテストに対する一般的な理解

これは個人的な意見ですが、スナップショットテストが React コンポーネント以外にどんな場面へ適用可能なのか、というのはあまり知られていないように思います (僕も含めて)。スナップショットテストは非常に優れた性質を持っており、様々な場面へ適用できる有用なテスト手法です。しかしスナップショットテストがどういった特性を持っていて、どういう向き不向きがあるのか、といったことはほとんど知られていません。ここぞという時に使えるよう、使い時くらいは分かっていると便利ですよね。

どういう場面では向いているか

という訳でここからは、スナップショットテストがどういった場面で有用なのかが分かるよう、スナップショットテストの向き不向きについて考えてみます。ざっとスナップショットテストが向いている場面について書き出すと、こんな感じでしょうか。

  • 出力に差分が出たことを検知したい時
    • 差分があった時に fail して警告ほしい、どういう差分が出たのか教えてほしい、みたいなケースを想定
  • 有意な出力を出せる時
    • もうちょっと噛み砕くと、出力にテストしたいことが埋め込まれていて、かつ人間の目に読みやすい形になっている時
    • 例えばコンパイラが出力するコンパイルエラーのメッセージにて、エラーの発生行数が期待通りに印字されているかをテストしたい、みたいなケースを考えます
      • この場合はターミナルに出力されたエラーメッセージに発生行数がerror: index.ts:10:3みたいな形式で埋め込まれているので、ターミナルへ出力されたテキストををそのままスナップショットとして取れれば良いです
      • 逆にエラーメッセージが何件出たかをこれでテストするのは難しいです
        • 沢山エラーメッセージが出ると巨大なスナップショットが生成されてしまいます
        • 人間の目で数えることになって大変
    • 素朴にexpect(typeCheck(program).errors.length).toEqual(100)のような assertion を書くくらいで十分なはず
  • テスト対象がテスタブルなインターフェイスを公開してくれていない時
    • Virtual DOM など
      • <TodoList items={items} />が render された結果に items がitems.length個含まれているかをテストしたい、みたいなケースを想定
      • jsdom をインストールして DOM API を使ってテストしようと思えばできなくはないです (expect(document.querySelectorAll('.item').length).toEqual(5);)
      • けど Virtual DOM をそのままシリアライズしてそのスナップショットを取ったほうがずっと簡単にテストできます
    • スナップショットテストであれば出力さえ取れればテストできるので、テスタブルなインターフェイスが無くても何とかなる可能性があります
  • テストがない朽ちたコードを触る時
    • 触ろうにもテストが無いのでまずはテストを追加したい / けどテストを追加しようにもユニットテストを差し込めるようなインターフェイスになっていない…みたいな状況を想像してください
    • こういう時スナップショットテストが役に立ちます
    • ロガーを用意して、そのロガーを使ってアプリケーションのあらゆる場所でデバッグ出力をして、その出力をスナップショットテストする、というもの
      • 何かの拍子でログ出力の順番が変わったり、出力の内容が変わった時に気づくことができます
      • アプリケーションコードにはログ出力するコードを差し込むだけで、インターフェイスを全く変更する必要が無いので安全かつ簡単に導入できます
    • Golden Master と呼ばれている手法らしいです

どういう場面では向いていないか

逆にどういう場面では向いていないかというと…

  • 上記のメリットを殆ど享受できない場合、スナップショットテストは向いてません
    • そんなの当たり前では、と思うかもしれないけど、この観点は意外と重要だったりします
    • というのもこういうケースの場合、他のテスト手法を採用したほうが適切な場合が多いため
    • 例えば 2 つの整数を受け取ってその和を返すadd関数のテストがしたくて、以下のようなテストを書いたとします
    • さて何が問題でしょうか
    • まず答えが何になるべきなのかスナップショットテストからは分かりません
      • パッと見では3になるべきですが、正確なことはスナップショットファイルを見に行かなければ分かりません
    • また、スナップショットテストにしてしまうと、正解のデータを書き換えるには一度テストを走らせる必要があります
      • expect(add(1, 2)).toEqual(3)ならexpect(add(1, 3)).toEqual(4)と書き換えるだけで良いはずでした
    • ...というようにテストとしては動いているけど、なんか過剰な感じがしてしまう
    • こういう場合は素朴に数値として assert したほうが、テストが落ちた時にデバッグしやすいし、テストが読みやすいし、スナップショットファイルも要らなくなってシンプルになります
    • 小学校レベルの知識で解けるのであれば、大学レベルの知識を使って解くのではなくてそっちを使って解きましょう、みたいな
    • ゴブリンを倒すのであれば大量に MP を消費する大魔法を使わずとも、錆びた剣を振りかざすので十分なはず
    • 乱用することなく、状況に応じて道具を使い分けるという話
  • 出力の差分が巨大になる場合、スナップショットテストは向いてません
    • 例: 2 京行の差分が出る時
    • 例: Visual Regression Test で、5GB の画像が出力される時
    • テストフレームワークが処理するのに時間が掛かるし、最悪の場合クラッシュします
  • 出力の差分を見て、その妥当性を判断するのが困難な場合、スナップショットテストは向いていません
    • 例: 5 万行の差分が出るテスト
    • 1 分で 50 行見たとしても見終わるのに 1000 分掛かります
      • 9 時に出社して差分を見始めて、お昼過ぎてもまだまだで、定時過ぎてもまだまだで、終電が無くなった頃にようやく見終わる、みたいなスケール
      • 機械より人間が頑張るスタイル
    • ただし差分の内容を見ずに変わったことだけ分かれば良い場合や、最初の 100 行だけ見て妥当ですねと判断できる場合であればこの限りではないです
  • 出力の変化が激しい場合
    • 例: 現在時刻を表示するクロックコンポーネントのテスト
      • テストを実行する時刻によって出力が変わって毎回テストが fail します
      • 毎回人の目でチェックしないといけません
      • 機械より人間が頑張るスタイル 2
      • ただし何らかの手段で出力を固定できれば、問題を回避できる場合があります
        • クロックコンポーネントの例では時間を司る API をモックして時刻を固定すれば良い
        • Visual Regression Test なら変化する部分を黒塗りしてから比較すれば良い
        • このような出力の固定化が困難な場合はスナップショットテスト以外のテスト手法を検討しましょう
    • 例: アプリケーションのソースコードそのもののスナップショットテスト
      • ソースコードを 1 文字書き換えたらテストが fail する、みたいな
      • 誰もそんなテスト書かないと思いますが…
      • 良い具体例が思いつかなかった!本当はちょっとどこかを弄ったら毎回 fail するのは良くないよね、みたいなことを言いたかった。

事例

今までで見つけた利用例などを書いてみます。

その他使えそうなところ

以上の情報を元に他にどういう場面で使えそうか、というのを考えてみます。

  • nginx が返すレスポンスのテスト
    • ある URL にリクエストを飛ばした時に、期待されるヘッダや body から構成されるレスポンスが返ってくるかをテストしたい、みたいな
    • ちゃんと期待するヘッダが付いてるレスポンスが返ってくるかなどをテストできるはず

自分を鼓舞する言葉を思い出しながら文章を書く

ここ一番は頑張りたいだとか、ここは力を入れてやりたいとか、そういった場面に遭遇する時が時々ある。最近だとイベントで発表する資料を作ったりだとか、卒業論文書いたりだとか、1つの技術をひたすら深堀りしていく記事を書いたりだとか。けどそういったものを成し遂げるのは本当に大変で、途中で何度も挫けそうになる。そこで挫けずにモチベーションを保ったまま進めるように、自分を鼓舞する言葉というのを集めていて、それを執筆中に思い出すようにしている。文章書いてる時に思い出す言葉を中心にリストアップしたけど、いくつかは普遍的な言葉なので、文章書く時以外でも使えるはず。

折角やるんだからより良くやろう/丁寧にやろう

折角文章書いたり発表するのだから全力でやろう。折角取り組むチャンスを得たのだから、その機会を大切にしよう、という考え。

これだと勿体ないよ

自分がしたことがちゃんと伝わるように書こう、折角面白いことしたのにそれが伝わらないのは勿体ないよ、という考え。元々学部の時に居た研究室の指導教官がよく言っていて、良い言葉だなと思ったので研究室を離れた後もずっと使っている。

言葉はごまんとあるのだから、今よりも良い表現がきっとあるはず

文章を推敲する時に思い出す言葉。何かイマイチな文章があるけど、どうやって書き換えたら良くなるのか全然思いつかない時に使う。

答えが見えていないものを推敲するのは多くの場合勇気が要る。いざ推敲にチャレンジしても対して良い文章にならないかもしれないし、もしかしたら悪化するかもしれない。どれだけ考えても良い表現を見つけられず、時間を浪費するだけ (そして締切までに残された時間が短くなるだけ) かもしれない。

そういう時によくこの言葉を思い出して、きっとより良い表現はあるので、恐れずにチャレンジしようと言い聞かせている *1

ここは重要なところなので丁寧に書こう

ここを丁寧に書けば読者の理解がより深まって、後続の文章がより効果的に理解されるようになってお得、という考え。とりあえずコスパが良い箇所なので最優先で取り組もう、という気持ちになれる。

これちょっと捻ってみませんか => もうちょっと捻ってみませんか => もう1回くらい捻りませんか => ラストもう1回

捻れば捻るほど面白くなるのでどんどん捻っていこうという考え。辛い作業なのでゲーム感覚で口ずさんでいくと気が楽になって良い。


自分を鼓舞する言葉に込めた本意

一見するとこれらの言葉は自分をキツく縛り付けるような厳しい言葉のように聞こえて、ついつい無理をしなければ!と考えてしまいがちだけど、個人的には是非その真逆の捉え方をしてほしいと思っている。

重要なことは、これらの言葉を使うのは苦しい思いをして良い文章を書くためのものではなく、気楽に良い文章を書くためのものであるということ。だから言葉も厳しいものというよりは、ポジティブで背中を押してくれるような表現から構成するようにしている。執筆作業の辛さを少しでも和らげるようなまじないを掛けることで、無理なくより良い文章を書くことができる。それがこれらの言葉に込めている本意です。

皆さんもオススメのまじないがあったら教えて下さい。

*1:実際に歯を食いしばって推敲するとより良い文章になるので、ただのまじないではないと思っている。

2020年ブックマークランキング

年間ブックマークランキングジェネレーター で作ってみた。手軽に1年を振り返れて便利。8位は去年に投稿した記事だけど何故かランキングに入っている。まあ気にしないことにします。

mizdra's blogの2020年ブックマークランキングベスト8(累計600ブックマーク)

# タイトル
1位 画像による Layout Shift が無くなる Web がやって来る - mizdra's blog
2位 polyfill を深堀りする - mizdra's blog
3位 趣味で創作する時は常に何かしら新しいことに挑戦する - mizdra's blog
4位 target=''_blank" な <a> タグに noopener だけでなく noreferrer も付けるべきか - mizdra's blog
5位 150B で動く strictly-typed な Event Emitter を作った - mizdra's blog
6位 Scrapboxのページ内に埋め込まれているアイコンをsuggestして挿入できるUserScript作った - mizdra's blog
7位 日向縁さんの笑い声について思いを馳せる - mizdra's blog
8位 OK Google, 今日のゆゆ式 - mizdra's blog

generated by 年間ブックマークランキングジェネレーター

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.