[JavaScript] Web Worker で大量テストデータ(CSV)を生成する


この記事は、 チームスピリット Advent Calendar 2017 の22日目の記事です。
https://adventar.org/calendars/2207
(遅くなってしまいました…!すみません!)

やりたいこと

CSVファイルを使用してデータをインポートする機能を作ったので、実際に大量のデータをインポートするテストをしたい。

要件

  • あらかじめ用意したヘッダ情報をもとに、大量(10000行×200列くらい)のCSVを生成したい
  • 自分以外の人にも使用してもらうため、面倒な環境構築なしで実行できるようにしたい

→ JavaScriptで実装してブラウザで実行できるようにしよう

普通に実装してみる

index.html
<html>
  <head>
    <meta charset="utf-8">
    <title>CSVgenerator</title>
  </head>
  <body>
    <p>
      <input type="text" id="rows" value="100"></p>
    <button id="execute">生成</button>
  </body>
</html>
index.js
import Papa from 'Papaparse'

import columnDefinition from '!json-loader!./ColumnDefinition.json'
import DataGenerator from 'DataGenerator'
import FileDownloader from 'FileDownloader'

document.getElementById('execute').addEventListener('click', () => {
  const rows = document.getElementById('rows').value

  const generator = new DataGenerator(columnDefinition)
  const data = generator.generate(rows)
  const csv = Papa.unparse(data)

  const downloader = new FileDownloader(csv, 'CSVテストデータ.csv')
  downloader.execute()
})
  • PapaparsePapa Parse (便利なCSVパーサ。今回はJavascript object → CSV の変換に利用)
  • columnDefinition … 予め用意したヘッダ情報
  • DataGenerator … 予め用意したデータ生成機能(今回は省略)
  • FileDownloader … 予め用意したファイルダウンロード機能(今回は省略)

実行すると、こうなりました ↓

少量ならぎりぎりいけますが、今回やりたいこと(10000行×200列)をやらせようとするとブラウザが応答しなくなってしまいます。

そこで、WebWorkerを使用して重い処理をバックグラウンド実行させるようにします。

WebWorkerとは

Web Worker とは、Web アプリケーションにおけるスクリプトの処理をメインとは別のスレッドに移し、バックグラウンドでの実行を可能にする仕組みのことです。 時間のかかる処理を別のスレッドに移すことが出来るため、UI を担当するメインスレッドの処理を中断・遅延させずに実行できるという利点があります。
Web Workers API | MDN

WebWorkerを使用すると、

  • 重い処理をメインスレッドと別のスレッドで実行させる1
    • →処理の最中でも、メインスレッド(ブラウザ内の実際のタブ)が停止しなくなる
  • 並列処理を行うことができる

といったメリットがあります

WebWorkerを実装する

  1. Worker に当たる部分を別ファイルに切り出します
  2. メインスレッド側の処理で Worker を生成します (Workerの相対パスが必要)
  3.  postMessage() でデータを送信
  4.  worker側は、 onmessage でデータを受信
index.js
const worker = new Worker('worker.js') // Worker を生成
document.getElementById('execute').addEventListener('click', () => {
  worker.postMessage({ columnDefinition, rows }) // Worker にデータを送信
})

// Worker からデータを受信
worker.onmessage = (event) => {
console.log(event.data)
  // do something
}
worker.js
onmessage = (event) => {
  const generator = new DataGenerator(event.data.columnDefinition)
  const data = generator.generate(event.data.rows)
  const csv = Papa.unparse(data)
  postMessage(csv)
}

また、以下のようにWorkerを複数生成すれば並列処理も可能です。2
Promise.all で結果を取得できます。

index.js
import columnDefinition from '!json-loader!./ColumnDefinition.json'

const MAX_WORKERS = 4 // CPUコア数によって調節
const rows = 10000 // 生成する行数

const promises = []

const chunkSize = Math.ceil(rows / MAX_promises)

for (let i = 0; i < MAX_WORKERS; i++) {
    const worker = new Worker('worker.js')

    const promise = new Promise((resolve, reject) => {
        worker.addEventListener('message', message => {
            resolve(message.data.csv)
        })
    })
    promises.push(promise)

    const from = i * chunkSize
    const to = Math.min(( i + 1 ) * chunkSize, rows)
    worker.postMessage({ columnDefinition, from, to })
}

Promise.all(promises).then(results => {
    const csv = results.join('\r\n')
    console.log(csv)
    // do something
})

WebWorkerの注意点

  • Worker の処理は、メインスレッド(親ページ)に直接影響を与えうる処理は実行できません。
    • DOMの操作
    • window オブジェクトへのアクセス(undefinedになります)
    • LocalStorage へのアクセス(undefinedになります)
      • ちなみに、 WebWorker の一種である Service Worker では、 Cache Storage という Local Storage に似た別のストレージを利用しているようです。
  • 上記の通り、 Workerは基本的に別ファイルに分割しておく必要があります。

Array Buffer による高速化?

https://qiita.com/Quramy/items/8c12e6c3ad208c97c99a こちらの記事を読むと、 Worker とのやりとりに Array Buffer (Transferable Object) を使用すると、転送オーバヘッドが小さくなるため処理が高速化するとのことです。

ただ、 Array Buffer を利用するには、 Uint8Array オブジェクトを利用する必要がある(つまり、整数値に変換してあげる必要がある)とのことで、今回のようにCSVデータ(String)を扱う場合は相性が悪そうです。

String と Uint16Array の変換は こちら のソースが利用できそうだったので、実際にこれを利用した方法でCSV生成を実行してみました。

(CSV10000行を生成するケース)

  • String を直に渡した場合: 約6500msec
  • Worker側でStringをUint16Arrayに変換して送信→メインスレッドでStringに戻した場合: 約27000msec

逆に遅くなってしまいました。
どうも、String と Uint16Array の変換はコストが極めて高いらしく、今回のケースでは高速化にはつながらなかったようです。
(おそらく、 String ではなく数値のみを受け渡す場合は高速化が期待できるはずです)

その他のWorker

今回紹介したのは、 Web Worker の中の Dedicated Worker という1種類だけであり、
Web Worker はそれ以外にも

といった種類があるようです。
まだこのあたりは試してみていないので、今後試したらまた結果を記事にできればと思います。


  1. これを聞いたとき、 「JavaScriptってシングルスレッドなのだから、マルチスレッドといっても擬似的なものなのでは?」と思ったのですが、Web Worker を使用する | MDN によると、WorkerのスレッドはOSレベルで生成されているようです。  

  2. こちらの記事を参考にしました。 https://sbfl.net/blog/2016/09/01/javascript-webworkers/