ts-morphのパフォーマンス改善

17.2K Views

November 16, 24

スライド概要

TS Kaigi Kansai 登壇資料

profile-image

Frontend engineer @lapras_inc / TypeScript / Vue.js / Firebase / 元消防士

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

ts-morphのパフォーマンス改善 2024/11/15 TS Kaigi Kansai @KawamataRyo

2.

@KawamataRyo LAPRAS株式会社 元消防士 2児の父 懸垂, 個人開発

3.

宣伝 Analyticsページの感想をシェアしてお肉ケーキをもらおう! Analyticsリリース記念キャンペーン Analytics 企業があなたのポートフォリオやアウトプットを観覧した履歴です。 26 22 18 14 10 8 4 0 7/31 8/7 8/14 7/31 8/28 9/4 9/11 9/18 9/25 10/2 10/9 10/16 10/23 興味通知受信 16回 アウトプット閲覧 33回 ポートフォリオ閲覧 158回 Analyticsページの感想をXにシェアで抽選5名様に松阪牛肉ケーキをプレゼント! キャンペーン期間 2024.11/13(水)~11/27(水) LAPRAS

4.

今日話すこと

5.

今日話すこと ts-morphを使ったスクリプトのパフォーマンスを改善するためのTips ■ ts-morphとは? ■ なぜパフォーマンス改善? ■ パフォーマンス改善のためのTips ■ 実際の改善例

6.

ts-morphとは?

7.

ts-morphとは? TypeScript Compiler APIをラップして、より直感的にコードを解析・操作できるようにしたライブラリ。 https://ts-morph.com/ dsherret/ts-morph TypeScript Compiler API wrapper for static analysis and programmatic code changes. 60 Contributors 145k Used by 5k Stars 195 Forks github.com GitHub - dsherret/ts-morph: TypeScript Compiler API wrapper for static analysis and programmatic code changes.

8.
[beta]
変数名を変換するコード (TypeScript Compiler API)
import * as ts from "typescript";
// ソースファイルを読み込む
const program = ts.createProgram(['./src/sample.ts'], {});
const sourceFile = program.getSourceFile(fileName);
// 変数宣言を探して名前を変更するtransformer
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => {
const visit = (node: ts.Node): ts.Node => {
if (ts.isIdentifier(node) && node.text === "oldname") {
return ts.factory.createIdentifier("newname");
}
return ts.visitEachChild(node, visit, context);
};
return (node: T) => ts.visitNode(node, visit) as T;
};
// 変換を実行
const transformedSourceFile = ts.transform(sourceFile!, [transformer]).transformed[ ];
// 変更後のコードを出力
const printer = ts.createPrinter();
const output = printer.printNode(ts.EmitHint.SourceFile, transformedSourceFile, sourceFile!);
console.log(output);
9.
[beta]
変数名を変換するコード (TypeScript Compiler API)
import * as ts from "typescript";
// ソースファイルを読み込む
const program = ts.createProgram(['./src/sample.ts'], {});
const sourceFile = program.getSourceFile(fileName);
// 変数宣言を探して名前を変更するtransformer
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => {
const visit = (node: ts.Node): ts.Node => {
if (ts.isIdentifier(node) && node.text === "oldname") {
return ts.factory.createIdentifier("newname");
}
return ts.visitEachChild(node, visit, context);
};
return (node: T) => ts.visitNode(node, visit) as T;
};
// 変換を実行
const transformedSourceFile = ts.transform(sourceFile!, [transformer]).transformed[ ];
// 変更後のコードを出力
const printer = ts.createPrinter();
const output = printer.printNode(ts.EmitHint.SourceFile, transformedSourceFile, sourceFile!);
console.log(output);
TransformerなどAST特有の操作が必要
10.

変数名を変換するコード (ts-morph) import { Project } from "ts-morph"; // プロジェクトを作成 const project = new Project(); // ソースファイルを追加 const sourceFile = project.addSourceFileAtPath("./src/sample.ts"); // 変数名を変更 sourceFile.getVariableDeclaration("oldname")?.rename("newname"); // 変更後のコードを出力 console.log(sourceFile.getText());

11.
[beta]
変数名を変換するコード (ts-morph)
import { Project } from "ts-morph";
// プロジェクトを作成
const project = new Project();
// ソースファイルを追加
const sourceFile = project.addSourceFileAtPath("./src/sample.ts");
// 変数名を変更
sourceFile.getVariableDeclaration("oldname")?.rename("newname");
// 変更後のコードを出力
console.log(sourceFile.getText());
普通のオブジェクトを扱うような操作感でASTを操作できる!
12.

なぜパフォーマンス改善?

13.

なぜパフォーマンス改善? ts-morphは便利な反面、抽象化のオーバーヘッドがあり素のCompiler APIに比べて若干処理が遅い。大規模なコードベースの解析や、CIなどで都度実行するスクリプトを作成する場合は問題になりやすい。

14.

パフォーマンス改善のためのTips

15.

パフォーマンス改善のためのTips 1. Work With Structures Instead 構造体を利用する 2. Batch operations バッチ処理を活用する 3. Analyze then Manipulate 解析と操作を分離する 参考: https://ts-morph.com/manipulation/performance

16.

パフォーマンス改善のためのTips 1. Work With Structures Instead 構造体を利用する 2. Batch operations バッチ処理を活用する 3. Analyze then Manipulate 解析と操作を分離する 参考: https://ts-morph.com/manipulation/performance

17.

解析と操作を分離するとは? 改善前 for (const sourceFile of sourceFiles) { for (const classDec of sourceFile.getClasses()) { // 解析 if (someCheckOnSymbol(classDec.getSymbolOrThrow()))) // 操作 classDec.remove(); } } 改善後 const classToRemove = [] // 解析 for (const sourceFile of sourceFiles) { for (const classDec of sourceFile.getClasses()) { if (someCheckOnSymbol(classDec.getSymbolOrThrow()))) classToRemove.push(classDec) } } // 操作 for (const classDec of classToRemove) { classDec.remove() }

18.

解析と操作を分離するとは? 改善前 for (const sourceFile of sourceFiles) { for (const classDec of sourceFile.getClasses()) { // 解析 if (someCheckOnSymbol(classDec.getSymbolOrThrow()))) // 操作 classDec.remove(); } } 改善後 const classToRemove = [] // 解析 for (const sourceFile of sourceFiles) { for (const classDec of sourceFile.getClasses()) { if (someCheckOnSymbol(classDec.getSymbolOrThrow()))) classToRemove.push(classDec) } } // 操作 for (const classDec of classToRemove) { classDec.remove() } ひとつのループの中で解析と操作を実行している

19.

解析と操作を分離するとは? 改善前 for (const sourceFile of sourceFiles) { for (const classDec of sourceFile.getClasses()) { // 解析 if (someCheckOnSymbol(classDec.getSymbolOrThrow()))) // 操作 classDec.remove(); } } 改善後 const classToRemove = [] // 解析 for (const sourceFile of sourceFiles) { for (const classDec of sourceFile.getClasses()) { if (someCheckOnSymbol(classDec.getSymbolOrThrow()))) classToRemove.push(classDec) } } // 操作 for (const classDec of classToRemove) { classDec.remove() } 解析と操作を別のループで実行している

20.

解析と操作を分離するとなぜ速くなる? ts-morphは操作の度にASTを再構築するため、解析と操作を一緒のループで行ってしまうと、解析時にASTのキャッシュが効かなくなる(操作によってASTが変わるため)。そのため解析と操作を分離して別のループで実行することでパフォーマンスが改善する。

21.

実際のスクリプトで試してみる

22.

対象のスクリプト suppress-ts-erros コードベースを解析して型エラーがある場合に、ts-expect-errorを自動挿入するCLIツール https://github.com/kawamataryo/suppress-ts-errors kawamataryo/suppress-ts-errors CLI tool to add @ts-expect-errors to typescript type errors 6 Contributors 1 Used by 121 Stars 9 Forks github.com GitHub - kawamataryo/suppress-ts-errors: CLI tool to add @ts-expect-errors to typescript type errors

23.
[beta]
修正コード (変更前)
for (const sourceFile of sourceFiles) {
const { text: textWithComment, count: insertedCommentCountPerFile } =
suppressTsErrors({sourceFile, commentType, withErrorCode: errorCode,});
if (insertedCommentCountPerFile > ) {
sourceFile.replaceWithText(textWithComment);
sourceFile.saveSync();
insertedCommentCount += insertedCommentCountPerFile;
}
}
24.
[beta]
修正コード (変更前)
for (const sourceFile of sourceFiles) {
const { text: textWithComment, count: insertedCommentCountPerFile } =
suppressTsErrors({sourceFile, commentType, withErrorCode: errorCode,});
if (insertedCommentCountPerFile > ) {
sourceFile.replaceWithText(textWithComment);
sourceFile.saveSync();
insertedCommentCount += insertedCommentCountPerFile;
}
}
一つのループの中で解析と操作を実行している
25.
[beta]
修正コード (変更後)
const targetFiles = []
// 解析
for (const sourceFile of sourceFiles) {
const { text: textWithComment, count: insertedCommentCountPerFile } =
suppressTsErrors({sourceFile, commentType, withErrorCode: errorCode,});
if (insertedCommentCountPerFile > ) {
targetFiles.push({
sourceFile,
textWithComment,
})
insertedCommentCount += insertedCommentCountPerFile;
}
}
// 操作
for (const targetFile of targetFiles) {
targetFile.sourceFile.replaceWithText(targetFile.textWithComment);
targetFile.sourceFile.saveSync();
}
26.
[beta]
修正コード (変更後)
const targetFiles = []
// 解析
for (const sourceFile of sourceFiles) {
const { text: textWithComment, count: insertedCommentCountPerFile } =
suppressTsErrors({sourceFile, commentType, withErrorCode: errorCode,});
if (insertedCommentCountPerFile > ) {
targetFiles.push({
sourceFile,
textWithComment,
})
insertedCommentCount += insertedCommentCountPerFile;
}
}
// 操作
for (const targetFile of targetFiles) {
targetFile.sourceFile.replaceWithText(targetFile.textWithComment);
targetFile.sourceFile.saveSync();
}
対象を配列に格納し、
解析と操作を別のループで実行している
27.

結果 Befre ~/g/g/l/scouty >>> hyperfine --warmup 1 --runs 5 './run_script.sh' Benchmark 1: ./run_script.sh Time (mean ± σ): 80.338 s ± 2.709 s [User: 90.110 s, System: 2.104 s] Range (min ... max): 77.732 s ... 83.528 s 5 runs After ~/g/g/l/scouty >>> hyperfine --warmup 1 --runs 5 './run_script.sh' Benchmark 1: ./run_script.sh Time (mean ± σ): 63.007 s ± 4.685 s [User: 68.292 s, System: 0.938 s] Range (min ... max): 56.062 s ... 69.038 s 5 runs

28.

結果 Befre ~/g/g/l/scouty >>> hyperfine --warmup 1 --runs 5 './run_script.sh' Benchmark 1: ./run_script.sh Time (mean ± σ): 80.338 s ± 2.709 s [User: 90.110 s, System: 2.104 s] Range (min ... max): 77.732 s ... 83.528 s 5 runs hyperfineのベンチマークで平均80秒かかる

29.
[beta]
結果
平均63秒に!! 1.2倍のパフォーマンス改善
After
~/g/g/l/scouty >>> hyperfine --warmup 1 --runs 5 './run_script.sh'
Benchmark 1: ./run_script.sh
Time (mean ± σ): 63.007 s ± 4.685 s [User: 68.292 s, System: 0.938 s]
Range (min ... max): 56.062 s ... 69.038 s 5 runs
30.

まとめ

31.

まとめ ts-morphは便利な反面、抽象化のオーバーヘッドがあり素のCompiler APIに比べて若干処理が遅い。大規模なコードベースの解析や、CIなどで都度実行するスクリプトを作成する場合は問題になりやすい。だが、少しコードの書き方を工夫することでパフォーマンスは改善できる。(自戒を込めて) Documentをよく読もう!!

32.

End👋