VS Codeのエージェントモードを拡張するには、を試してください!

名前マングリングでVS Codeを縮小

2023年7月20日 マット・ビアナー、@mattbierner

最近、Visual Studio Codeが提供するJavaScriptのサイズを20%削減しました。これは3.9MB強の節約になります。確かにリリースノートの一部の個別のGIFよりは小さいですが、それでも決して無視できない量です!この削減は、ダウンロードしてディスクに保存する必要があるコードが少なくなるだけでなく、JavaScriptが実行される前にスキャンするソースコードが少なくなるため、起動時間も改善されます。コードを一切削除することなく、またコードベースに大きなリファクタリングを行うことなくこの削減を達成したことを考えると、これは非常に素晴らしいことです。代わりに必要だったのは、新しいビルドステップ、つまり名前マングリングだけでした。

この記事では、私たちがどのようにこの最適化の機会を特定し、問題へのアプローチを検討し、最終的にこの20%のサイズ削減を実現したかをお話ししたいと思います。マングリングの詳細に焦点を当てるのではなく、VS Codeチームがどのようにエンジニアリング問題に取り組むかのケーススタディとしてこれを扱いたいと思います。名前マングリングは巧妙なテクニックですが、多くのコードベースではそれに見合う価値がない場合があり、私たちの特定のマングリングアプローチは改善の余地があるか、(プロジェクトのビルド方法によっては)全く必要ないかもしれません。

問題の特定

VS Codeチームは、ホットなコードパスの最適化、UIの再レイアウト削減、起動時間の高速化といったパフォーマンスに情熱を注いでいます。この情熱には、VS CodeのJavaScriptのサイズを小さく保つことも含まれます。デスクトップアプリケーションに加えて、VS Codeがウェブ(https://vscode.dev)で提供されるようになったことで、コードサイズはさらに重要な焦点となっています。コードサイズを積極的に監視することで、VS Codeチームのメンバーは変更があった際にそれを認識できます。

残念ながら、これらの変更はほぼ常に増加でした。VS Codeにどのような機能を組み込むかについて多くの考慮を払っていますが、長年にわたって新しい機能を追加することで、提供するコードの量は必然的に増大しました。例えば、VS Codeの主要なJavaScriptファイルの一つであるworkbench.jsは、8年前と比べて約4倍のサイズになっています。今日では多くの人が必須と考える機能(エディタータブや組み込みターミナルなど)が8年前のVS Codeにはなかったことを考えると、その増加は聞こえるほどひどいものではないかもしれませんが、無視できるものでもありません。

The size of 'workbench.js' has slowly increased over the past eight years

この4倍のサイズ増加は、継続的な多くのパフォーマンスエンジニアリング作業を経た上でのものです。繰り返しになりますが、この作業は主にコードサイズを追跡しており、その増加を見ることを本当に嫌がっているために行われます。私たちはすでに、コードをesbuildに通してミニファイするなど、多くの簡単なコードサイズ最適化を行ってきました。長年にわたり、さらなる節約を見つけることはますます困難になっています。多くの潜在的な節約は、それがもたらすリスクや、それを実装し維持するために必要な追加のエンジニアリング努力に見合わない場合もあります。このため、私たちはJavaScriptのサイズがゆっくりと増えていくのを見守らざるを得ませんでした。

しかし昨年、vscode.devでミニファイされたソースコードをデバッグしている際に、驚くべきことに気づきました。ミニファイされたJavaScriptには、extensionIgnoredRecommendationsServiceのような長い識別子名が大量に含まれていたのです。これには驚きました。esbuildがすでにこれらの識別子を短縮しているものとばかり思っていました。そして、esbuildは実際に「マングリング」(JavaScriptツールがコンパイル言語でのわずかに似たプロセスから借用したと思われる用語)と呼ばれるプロセスを通じて、場合によっては識別子を短縮していることが判明しました。

ミニファイ中、マングリングは長い識別子名を短縮し、以下のようなコードを

const someLongVariableName = 123;
console.log(someLongVariableName);

はるかに短いものに変換します

const x = 123;
console.log(x);

JavaScriptはソーステキストとして配布されるため、識別子名の長さを短縮することは、実際にプログラムのサイズを減少させます。コンパイル言語に慣れている方には、この最適化は少し馬鹿げているように見えるかもしれませんが、JavaScriptの素晴らしい世界では、このようなメリットはどこで見つけても喜んで受け入れます!

さて、皆さんが急いで全ての変数を1文字にリネームし始める前に、このような最適化は慎重に取り組む必要があることを強調したいと思います。もし潜在的な最適化がソースコードの可読性や保守性を低下させたり、大幅な手作業を必要とする場合、本当に驚異的な改善をもたらさない限り、それを行う価値はほとんどありません。数バイトをあちこちで削減するのは良いことですが、驚異的とは言えません。

ビルドツールが自動的にそれらを行ってくれるなど、このような素晴らしい最適化を実質的に無料で得られるのであれば、その計算は変わってきます。そして実際、esbuildのようなスマートなツールはすでに識別子マングリングを実装しています。つまり、私たちはveryLongAndDescriptiveNamesThatWouldMakeEvenObjectiveCProgrammersBlushのような長い記述的な名前を書き続け、ビルドツールにそれらを短縮させることができるのです!

esbuildはマングリングを実装していますが、デフォルトでは、マングリングがコードの動作を変更しないと確信できる場合にのみ名前をマングリングします。結局のところ、バンドラーがコードを壊すのは本当に嫌なことです。実際には、これはesbuildがローカル変数名と引数名をマングリングすることを意味します。これは、コードが本当にばかげたことをしていない限り安全です(そのような場合、コードサイズよりもはるかに大きな問題を抱えている可能性が高いでしょう)。

しかし、esbuildの保守的なアプローチは、多くの名前の変更が安全であると確信できないため、マングリングをスキップすることを意味します。物事がうまくいかない可能性のある単純な例として、以下を考えてみましょう。

const obj = { longPropertyName: 123 };

function lookup(prop) {
  return obj[prop];
}

console.log(lookup('longPropertyName'));

もしマングリングがlongPropertyNamexに変更した場合、次の行の動的なルックアップは動作しなくなります

const obj = { x: 123 }; // Here `longPropertyName` gets rewritten to `x`

function lookup(prop) {
  return obj[prop];
}

console.log(lookup('longPropertyName')); // But this reference doesn't and now the lookup is broken

上記のコードでは、プロパティ自体がマングリング中に変更されたにもかかわらず、longPropertyNameを使用してプロパティにアクセスしようとしていることに注目してください。

この例は作り話ですが、実際のコードでは、このような破壊が起こる方法はたくさんあります。

  • 動的なプロパティアクセス。
  • オブジェクトのシリアライズ、または期待されるオブジェクト形状へのJSONパース。
  • 公開するAPI(利用者は新しいマングリングされた名前を知りません)。
  • 利用するAPI(DOM APIを含む)。

esbuildに検出したほぼすべての名前を強制的にマングリングさせることはできますが、そうすると上記の理由によりVS Codeは完全に壊れてしまいます。

それにもかかわらず、VS Codeのコードベースではもっとうまくやれるはずだという思いが拭いきれませんでした。もしすべての名前をマングリングできないのであれば、少なくとも安全にマングリングできる名前のサブセットを見つけられるのではないかと考えました。

プライベートプロパティでの試行錯誤

ミニファイされたソースコードを改めて見てみると、_で始まる長い名前が非常に多いことに気づきました。慣例的に、これはプライベートプロパティを示します。プライベートプロパティは安全にマングリングでき、クラス外のコードは何も気づかないはずですよね?それに待てよ、esbuildはすでにこれをやってくれているはずではないか?しかし、esbuildの開発者たちが怠け者ではないことは知っていました。もしesbuildがプライベートプロパティをマングリングしていないのであれば、それはほぼ間違いなく正当な理由があるからだと考えました。

問題をさらに考えると、プライベートプロパティも、上記のlongPropertyNameの例で示された動的なプロパティルックアップの問題の影響を受けることに気づきました。あなたのような賢いTypeScriptプログラマーは決してそのようなコードを書かないでしょうが、現実世界のコードベースでは動的なパターンが十分に一般的であるため、esbuildは安全策をとることを選択しています。

また、TypeScriptのprivateキーワードは、実際には単なる「丁寧な提案」に過ぎないということを覚えておいてください。TypeScriptコードがJavaScriptにコンパイルされると、privateキーワードは基本的に削除されます。つまり、クラス外の「無作法な」コードが、好き勝手に内部に入り込んでプライベートプロパティにアクセスするのを止めるものは何もないのです。

class Foo {
  private bar = 123;
}

const foo: any = new Foo();
console.log(foo.bar);

皆さんのコードが直接このような疑わしいことをしていないことを願いますが、プロパティ名を不用意に変更すると、オブジェクトスプレッド、シリアライゼーション、異なるクラスが共通のプロパティ名を共有している場合など、多くの予期せぬ厄介な形で問題を引き起こす可能性があります。

幸いなことに、VS Codeでは大きな利点があることに気づきました。それは、(ほとんど)健全なコードベースで作業しているということです。esbuildができない多くの仮定、例えば動的なプライベートプロパティアクセスや不適切なanyアクセスがないことなどを、私は立てることができました。これにより、私が直面していた問題はさらに単純化されました。

そこで、ヨハネス・リーケン(@johannesrieken)と私は一緒にプライベートプロパティのマングリングの検討を始めました。最初のアイデアは、コードベースのあらゆる場所でJavaScriptのネイティブな#privateフィールドを採用してみることでした。プライベートフィールドは上記で詳述されたすべての問題に影響されないだけでなく、esbuildによって自動的にマングリングされます。昔ながらのプレーンなJavaScriptに近づくことも魅力的でした。

しかし、このアプローチは、パラメータープロパティのすべての使用箇所を削除するなど、大規模な(つまりリスクの高い)コード変更を必要とするため、すぐに却下しました。比較的新しい機能であるため、プライベートフィールドはまだすべてのランタイムで最適化されているわけではありません。これらを使用すると、無視できる程度のものから約95%もの実行速度の低下を引き起こす可能性があります!長期的にはこれが正しい変更であるかもしれませんが、現状では私たちが必要とするものではありませんでした。

次に、esbuildが与えられた正規表現に一致するプロパティを選択的にマングリングできることを発見しました。しかし、この正規表現は識別子名にのみ一致します。これは、そのプロパティがTypeScriptでprivateとして宣言されているかどうかを知ることはできないことを意味しましたが、私たちは_で始まるすべてのプロパティをマングリングすることを試みることができました。これはプライベートおよび保護されたプロパティのみを含むことを期待していました。

すぐに、すべての_プロパティがマングリングされた動作するビルドが完成しました。良い感じです!これにより、プライベートプロパティのマングリングが可能であることが証明され、期待していたほどではないにせよ、それなりの節約が実現しました。

残念ながら、名前のみに基づくマングリングにはいくつかの深刻な欠点があります。これには、私たちのコードベースのすべてのプライベートプロパティが_で始まる必要があるという要件が含まれます。VS Codeのコードベースはこの命名規則を一貫して守っておらず、また、_で始まるパブリックプロパティがいくつか存在する場所もあります(これは通常、プロパティが外部からアクセス可能である必要があるが、APIとして扱われるべきではない場合、例えばテストなどで使用されます)。

また、マングリングされたコードが実際に正しいのか、完全に確信が持てませんでした。もちろん、テストを実行したりVS Codeを起動してみることもできましたが、これは時間がかかり、あまり一般的でないコードパスを見落とす可能性がありました。他のコードに触れることなく、プライベートプロパティのみをマングリングしていると100%確信することはできませんでした。このアプローチは、リスクが高すぎると同時に、採用するには負担が大きすぎると感じられました。

TypeScriptで自信を持ってマングリング

マングリングビルドステップに、より自信を持つ方法について考えていたところ、新しいアイデアを思いつきました。TypeScriptがマングリングされたコードを検証してくれたらどうだろうか?TypeScriptが通常のコードで未知のプロパティアクセスを捕捉できるのと同様に、TypeScriptコンパイラは、プロパティがマングリングされたものの、それへの参照が正しく更新されていないケースを捕捉できるはずです。コンパイルされたJavaScriptをマングリングする代わりに、TypeScriptのソースコードをマングリングし、その後、マングリングされた識別子名を持つ新しいTypeScriptをコンパイルすることができます。マングリングされたソースコードに対するコンパイルステップは、誤ってコードを壊していないというはるかに大きな自信を私たちに与えるでしょう。

それだけでなく、TypeScriptを使用することで、すべてのprivateプロパティを(単に_で始まるプロパティではなく)真に発見することができました。さらに、TypeScriptの既存のrename機能を利用して、予期せぬ形でオブジェクトの形状を変更することなく、シンボルをスマートにリネームすることもできました。

この新しいアプローチを試すことに熱心で、私たちはすぐに、およそこのように機能する新しいマングリングビルドステップを考案しました。

for each private or protected property in codebase (found using TypeScript's AST):
    if the property should be mangled:
        Compute a new name by looking for an unused symbol name
        Use TypeScript to generate a rename edit for all references to the property

Apply all rename edits to our typescript source

Compile the new edited TypeScript sources with the mangled names

そして、そのような素朴に見えるアプローチにしては、いくらか驚くべきことに、機能しました!少なくとも、ほとんどは。

TypeScriptが私たちのコードベース全体にわたって何千もの正しい編集を生成できたことに間違いなく感銘を受けましたが、いくつかのエッジケースを処理するためのロジックも追加する必要がありました。

  • 新しいプライベートプロパティ名が現在のクラス内で一意であるだけでは不十分で、現在のクラスのすべてのスーパークラスおよびサブクラス全体で一意である必要もあります。繰り返しますが、根本原因は、TypeScriptのprivateキーワードが単なるコンパイル時の装飾であり、スーパークラスやサブクラスがプライベートプロパティにアクセスできないことを実際に強制するものではないという点にあります。注意を払わないと、名前の変更が名前の衝突を引き起こす可能性があります(幸いなことに、TypeScriptはこれらをエラーとして報告します)。

  • 私たちのコードのいくつかの場所では、サブクラスが継承された保護プロパティを公開していました。これらの多くは間違いでしたが、これらのケースではマングリングを無効にするコードも追加しました。

これらのケースに対応するコードを追加した後、すぐに動作するビルドができました。プライベートプロパティをマングリングすることで、VS Codeのメインスクリプトであるworkbench.jsのサイズは12.3 MBから10.6 MBになり、約14%の削減となりました。これにより、スキャンするソーステキストが少なくなるため、コードの読み込みも5%高速化されました。私たちのソースにおけるいくつかの非常に軽微な安全でないパターンの修正を除けば、これらの節約は基本的に無料であったことを考えると、これはまったく悪くありません。

学びと今後の作業

プライベートプロパティのマングリングは、大規模なコード変更や高価な書き換えに頼ることなく、VS Codeでまだ大きな改善が見つけられることを示しています。この件に関しては、長年にわたり他の開発者もVS Codeのミニファイされたソースを見て、あの長い名前について疑問に思っていたのではないかと推測します。しかし、これを安全に行うことは不可能に見えたか、あるいは潜在的に大規模なエンジニアリング投資に見合わないと思われていたのかもしれません。

今回の成功の鍵は、名前マングリングが安全である可能性が高く、かつ最適化によって大きな改善がもたらされるケース(プライベートプロパティ)を特定したことでした。次に、この変更を可能な限り安全に行う方法を検討しました。これは、まずTypeScriptのツールを使用して自信を持って識別子をリネームし、次に再度TypeScriptを使用して、新しくマングリングされたソースコードが正しくコンパイルされることを確認することを意味しました。その過程で、私たちのコードがすでにほとんどのTypeScriptのベストプラクティスに従っており、またVS Codeの多くの一般的なコードパスをカバーするテストが整備されていたことが大いに役立ちました。これらすべてがうまく組み合わさり、Johと私は空き時間を使って、VS Codeで作業する他の開発者にほとんど影響を与えることなく、かなり抜本的な変更を実現することができました。

しかし、マングリングの話はこれで終わりではありません。新しくマングリングされミニファイされたソースを調べてみると、provideWorkspaceTrustExtensionProposalsやその他多くの長い名前を見つけて落胆しました。最も注目すべきは、localize(UIに表示される文字列に使用する関数)が約5000回も出現していたことです。明らかに、まだ改善の余地がありました。

プライベートプロパティのマングリングと同じアプローチと手法を用いて、私はすぐに、投資収益率が高く安全にマングリングできる別の一般的なコードパターン、つまりエクスポートされたシンボル名を見つけました。エクスポートが内部でのみ使用されている限り、コードの動作を変更することなくそれらを短縮できると確信しました。

これは概ね正しいと証明されましたが、やはりいくつかの複雑な点がありました。例えば、拡張機能が使用するAPIに誤って触れないように注意する必要がありました。また、TypeScriptからエクスポートされているものの、型なしJavaScriptから呼び出される一部のシンボル(通常はワーカースレッドやプロセスのエントリポイントとなるもの)を除外する必要もありました。

エクスポートのマングリング作業は前回のイテレーションで出荷され、workbench.jsのサイズを10.6 MBから9.8 MBへとさらに削減しました。すべての削減を合わせると、このファイルはマングリングなしの場合と比較して20%小さくなりました。VS Code全体では、マングリングによってコンパイル済みソースから3.9 MBのJavaScriptコードが削除されます。これはダウンロードサイズとインストールサイズの削減に良いだけでなく、VS Codeを起動するたびにスキャンする必要があるJavaScriptが3.9 MB少なくなることを意味します。

このチャートは、workbench.jsの時間の経過に伴うサイズを示しています。右側の2つのドロップに注目してください。VS Code 1.74での最初の大幅な低下は、プライベートプロパティのマングリングによるものです。1.80での2番目の小さな低下は、エクスポートのマングリングによるものです。

Zoomed in chart showing the drops from mangling

The size of 'workbench.js' over all VS Code releases, including the mangling work

私たちのミニファイされたソースにはまだ多くの長い名前が含まれているため、私たちのマングリング実装は間違いなく改善の余地があります。もしそれが価値があると思われ、かつ安全なアプローチを考案できれば、これらをさらに調査するかもしれません。理想的には、いつかこの作業の多くは全く必要なくなるでしょう。ネイティブのプライベートプロパティはすでに自動的にマングリングされており、私たちのビルドツールはコードベース全体でコードを最適化するのがうまくなることを願っています。現在のマングリング実装を確認できます。

私たちは常にVS Codeと私たちのコードベースをより良くするために努力しており、マングリングの作業は私たちがどのようにこれに取り組むかの素晴らしい例だと考えています。最適化は一度きりのものではなく、継続的なプロセスです。コードサイズを継続的に監視することで、時間の経過とともにどのように増加しているかを認識していました。この認識は、コードサイズが現状以上に膨張するのを防ぐのに間違いなく役立ち、また、常に改善を求めるよう私たちを促します。マングリングは魅力的なテクニックに見えましたが、当初は真剣に検討するにはリスクが高すぎました。このリスクを減らし、適切なセーフティネットを構築し、マングリング導入のコストをほぼゼロにした上で初めて、ビルドでそれを有効にするのに十分な自信を持てました。私は最終的な結果を非常に誇りに思っており、それを達成するまでの過程も同様に誇りに思っています。

ハッピーコーディング、

マット・ビアナー、VS Codeチームメンバー @mattbierner


マングリングの実装で重要な役割を果たしたヨハネス・リーケン氏、マングリングを安全に実装できるツールを構築してくれたTypeScriptチーム、驚くほど高速なバンドラーであるesbuild、そして、このような最適化に適したコードベースを構築してくれたVS Codeチーム全体に感謝します。そして最後に、何よりも重要なことですが、私たちが投げかける山のようなひどくマングリングされたJavaScriptにもかかわらず、常に私たちを高速に見せてくれるV8チームと他のすべてのJSエンジンに心からの感謝を捧げます。