🎭

TypeScriptによるseedデータの作成を130sから23sまで短縮した

2025/02/22に公開

業務の中で、seedデータの作成を130sから23sに短縮しました
今回は、その過程を記載していきます
↓改善前

前提

アプリケーションサーバーにNestjs、ORMはprismaを使用しています。

当初の課題

当初は、プロジェクト内で使用しているあるseedファイルの実行スピードが遅かったため(平均15sほど)、それを改善することを目的としてタスクに取りくみました。
対象のseedファイルをのぞいてみると下記のような実装がされていました。

parent.seed.ts

   await prisma.parent.update({
      where: { id: parent.id },
      data: {
        child1: {
          createMany: {
            data: [
              // 複数データ
            ],
          },
        },
        child2: {
          createMany: {
            data: [
              // 複数データ
            ],
          },
        },
        child3: {
          createMany: {
            data: [
              // 複数データ
            ],
          },
        },
        child4: {
          createMany: {
            data: [
              // 複数データ
            ],
          },
        },
        child5: {
          createMany: {
            data: [
              // 複数データ
            ],
          },
        },
        // その他、子テーブル群
      },
    })

パフォーマンスが低下しているということは、createManyが逐次実行されているのでは?という仮説を立て、一旦Promise.allで、並列処理にしてみて、ベンチマークしてみることにしました。

parent.seed.ts

    await Promise.all([
      prisma.child1.createMany({
        data: [
          // 複数データ
        ],
      }),
      prisma.child2.createMany({
        data: [
          // 複数データ
        ],
      }),
      prisma.child3.createMany({
        data: [
          // 複数データ
        ],
      }),
      prisma.child4.createMany({
        data: [
          // 複数データ
        ],
      }),
      prisma.child5.createMany({
        data: [
          // 複数データ
        ],
      }),
      // その他、子テーブル群
    ])

結果としては多少速くなりましたが、誤差の範囲でほぼ変わらずでした。
ここで、そもそも、このクエリ実行自体が原因ではなく、別の要因があるのでは?と考え、一度クエリの速度を計測してみたところ、200msほどでした。
やはり別の要因があるっぽいなと、各処理の実行速度をログに出力したところ、nestjsのtestingModuleの作成に時間がかかっていることが判明しました


  const module = await Test.createTestingModule({
    providers: [
      Service1,
      Service2,
      Service3
    ],
  }).compile()

プロジェクト内の全てのseedファイルで、testingModuleの作成が行われていたため、他のseedファイルも速度が遅かったのですが、とりわけ今回課題となっていたファイルが遅かったのは、おそらく、依存モジュールが多かったことが原因でした。

なので、このモジュールの作成を別の関数に切り分け、全seedファイルの実行前に行なってしまうことで、全てのseedの実行速度が改善されそうと考えました。

seed全体のパフォーマンス改善に取り組む

let moduleInstance: TestingModule | null = null

export async function getTestingModule(): Promise<TestingModule> {
  if (!moduleInstance) {
    moduleInstance = await Test.createTestingModule({
      providers: [
        Service1,
        Service2,
        Service3,
        Service4,
        Service5,
        Service6,
        Service7,
      ],
    }).compile()
  }
  return moduleInstance
}

export async function cleanupModule(): Promise<void> {
  if (moduleInstance) {
    await moduleInstance.close()
    moduleInstance = null
  }
}

このように、moduleの作成処理をgetTestingModuleに切り分け、作成処理は最初の1度だけ行われるようにしました。
あとは、この関数を各seedで呼び出すように修正すれば、パフォーマンスが改善されるはず、、、と考えました。が、結果は変わりませんでした。
ログを仕込んで確認してみると、関数に切り分けたのにも関わらず、相も変わらず、全ファイルで都度testingModuleの作成が行われていました。

今度は、挙動的にメモリがファイル間で共有されていないっぽいなという仮説が立ちました。実際に各seedファイル上でprosessのidを出力させたところ、それぞれ別のidが割り振られていました。
なぜこのようなことが起こっているかを深掘っていくと、seedの実行スクリプトが、それぞれのseedファイルに子プロセスを割り振るような処理をしていることがわかりました。

なので、下記のようにseedファイルの実行スクリプトを記述しました
本質的な部分のみをぬきとって記述していますが、実際には引数を受け取って特定のファイル群を実行できるようにするなど、もう少し利便性を高めたものを実装しました
(なぜseedsにマッピングしているかというと、CLI上でファイル名とその実行速度をログ出力したかったためです)


import { cleanupModule, getTestingModule } from './module'

import { bootstrap as bootstrap0 } from './path/to/seed0'
import { bootstrap as bootstrap1 } from './path/to/seed1'
import { bootstrap as bootstrap2 } from './path/to/seed2'
import { bootstrap as bootstrap3 } from './path/to/seed3'


export const seeds = {
  './path/to/seed0': bootstrap0,
  './path/to/seed1': bootstrap1,
  './path/to/seed2': bootstrap2,
  './path/to/seed3': bootstrap3,
}

async function runSeeds() {
  for (const [name, bootstrap] of Object.entries(seeds)) {
    const seedStart = new Date()
    await bootstrap()
    const seedEnd = new Date()
    const duration = seedEnd.getTime() - seedStart.getTime()
    console.info(`${name}`, `(${duration}ms)`)
  }
}

async function main() {
  const startOfAll = new Date()
  await getTestingModule()
  await runSeed(seeds['00.admin'])
  

  const totalTime = (Date.now() - startOfAll.getTime()) / 1000
  await cleanupModule()
  process.exit(0)
}

main()

このスクリプトにより、seedデータの作成を実行したところ、、、

23s。想像以上に、実行速度が上がりました。嬉しい

パフォーマンス改善は達成したが、新たな問題が発生

もしかしたら、お気づきになった方もいらっしゃるかもしれませんが、上記の実行スクリプトには1つ問題があります。
それは、seedファイルを新たに作成する都度、bootstrap関数のimportと、seedsへマッピングをしなければならないことです。
これは地味に面倒ですし、つい忘れてしまいそうです。

まず、最初に思いつくのは、seedファイル群が格納されいてるルートディレクトリを起点に、再起的にseedファイルを取得して、ファイルを実行するように修正することです
これによって、開発者はseedファイルの作成だけをすればよくなり、他のことは意識せずすみます。
実際にそのような実装を試しましたが、seedデータの作成処理が55sほどに伸びてしまいました。
当初の130sと比較すれば十分に早いですが、23sを知った身としては、もう遅く感じてしまいます。
代替案を考えます。

最終的な落とし所

最終的には、コマンド1発で、bootstrapのimportとseedsオブジェクトへのマッピングを自動で行ってくれる生成スクリプトを実装し、そのコマンドをMakefileに追加しました。
これも、seedファイルを追加した後、コマンドを実行しなければならない手間を増やしてしまっていますが、許容範囲内ということで、ここを落とし所としました。

感想

当初、1つのseedファイルのパフォーマンスを改善するのみだったはずが、少し大掛かりになってしまいました。しかし、結果として、seedデータの作成がかなり短縮され、開発におけるストレスが少し減らすことができました。
JSやTSの知見を深めた先で、自動生成スクリプトすら必要ない状態へと改善することは今後の自分への宿題とします。

GitHubで編集を提案

Discussion