1億行のJSONファイルを作ってみた

![_734c63d5-d192-439a-9726-3d8c45ffc7f6.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183059/b1b808b7-addf-0e10-c885-d843ca3d4012.jpeg)

DALL-E3に 「1億行のjsonファイル」 でイメージ生成した画像のうちの1つ 

# モチベーション

大規模なJSONデータを処理できるようになれば、それをS3に置くだけでDBになるのでは。
S3のバージョニング使えれば。。。

と夢が膨らんでいきそうですが、まずは、大規模なJSON作ってみよう。

# 大規模なJSONの定義

「VSCodeで開くことが出来ないJSONファイル」と定義します。

重いJSONをVSCodeで開こうとすると、下記のようなアラートが出てきます。

「それでも開く」 はVSCode終了のボタンとほぼ同義です。

![スクリーンショット 2023-12-04 10.43.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183059/1a3d6b22-f52e-3a14-2232-7001c388c4d4.png)

## VSCodeで開けないサイズ

VSCodeで開けるファイルサイズを設定できるようなのですが、設定が見つからず。。。
「VSCodeで開けないサイズ」＝「大規模」と呼ぶことにします。

自分の`MacbookPro M1 Max` のスペック

- OS Sonoma 14.1
- メモリ64GB
- ストレージ1TB

### 各データ量比較

- 100万行 (0.2GB)： 開く
- 1000万行 (2.6GB)： 「それでも開く」ボタン表示、押すとVSCode終了しない
- 1億行 (50.2GB)： 「それでも開く」ボタン表示、押すとVSCode終了

作るデータ量は1億行に決定しました。

# JSONの定義

一般的な偽ユーザーデータを作ります。
偽データを生成するのに少量であれば、ChatGPTに作ってもらいますが、さすがに1億行は作ってくれないので、[fakerjs](https://fakerjs.dev/)を使いました。

## fakerjsを使う

fakerjsはランダムなデータを作ってくれる便利なライブラリです。

```typescript
import { faker } from '@faker-js/faker/locale/ja';

export const data = () => ({
  id: `${faker.string.uuid()}`,
  name: `${faker.person.lastName()} ${faker.person.firstName()}`,
  kana: `${faker.person.lastName()} ${faker.person.firstName()}`,
  email: faker.internet.email(),
  company_name: faker.company.name(),
  joined_at: faker.date.recent(),
  created_at: faker.date.past(),
  updated_at: faker.date.past(),
});

export type Data = typeof data;
export type Schema = ReturnType<Data>;
```

## 壁その1：コールスタックの問題

fakerjsで返すデータを決めたら、あとは作りたい数字渡して配列いれて終わりみたいな簡単なコード書くと、配列をメモリ空間に保持しようとしたり、コールスタックエラー(`Maximum call stack size exceeded`)みたいなのが発生し異常終了しました。

```typescript
  const generateData = (num: number, schema: () => Schema) => {
    for (let i = 0; i < num; i++) {
      return schema();
    }
  }
```

なので、下記のように、データを生成したらコールスタックを積み上げていかないように、ジェネレーター関数を使って生成するように変更しました。

```typescript
  function *generateData(num: number, schema: () => Schema) {
    for (let i = 0; i < num; i++) {
      yield schema();
    }
  }
```

## 壁その2：メモリの問題

1. ジェネレーター関数で作ったデータ　
1. 配列にpush * 1億回　
1. ファイルに書き込む


みたいな素直なコードを書くと、配列に1億行pushして後（メモリに保持できずに）に異常終了します(100万行まではいけた)

そのメモリ対策として、NodeJS の [Stream](https://nodejs.org/api/stream.html) というAPIを使って、データをメモリにもたずに生成した側からファイルに書き込みを行いました。
[Stream](https://nodejs.org/api/stream.html)を使って、ジェネレーター関数で生成したオブジェクトをそのまま、ファイルに書き込んでいく流れになります。

```typescript
import { Stream } from 'stream';
import { Schema } from './schema';
import { data } from './schema';
import { createWriteStream } from 'fs';

const generateLength = 100000000;

function* generateData(num: number, schema: () => Schema) {
  for (let i = 0; i < num; i++) {
    yield schema();
  }
}

const firstLine = '[';
const newLine = '\n';
const comma = ',';
const lastLine = ']';

const readableStream = Stream.Readable.from(generateData(generateLength, data), { objectMode: true });
const writableStream = createWriteStream(`${generateLength}-data.json`);

writableStream.write(firstLine); // start of array

const counter = { input: 0 };

let isFirstChunk = true;

readableStream
  .on('data', (data) => {
    if (!isFirstChunk) writableStream.write(`${comma}${newLine}`);

    writableStream.write(JSON.stringify(data));
    counter.input++;
    console.clear();
    console.log('Input Generating...', counter.input, `/${generateLength}`);
    isFirstChunk = false;
  })
  .on('end', () => {
    writableStream.write(`${lastLine}${newLine}`);
    writableStream.end();
  });

writableStream.on('finish', () => {
  console.timeEnd('generate');
});
```

## 壁その3：生成速度と書き込み速度の不均衡

上記のコードだけでは、生成速度 > 書き込み速度 になって
そのうち、書き込み速度が追いつかず、6000万行あたりで止まってしまいました。
再度書き込み可能になったら、処理を再開する処理を追加します

```diff_typescript
import { Stream } from 'stream';
import { Schema } from './schema';
import { data } from './schema';
import { createWriteStream } from 'fs';

const generateLength = 100000000;

function* generateData(num: number, schema: () => Schema) {
  for (let i = 0; i < num; i++) {
    yield schema();
  }
}

const firstLine = '[';
const newLine = '\n';
const comma = ',';
const lastLine = ']';

const readableStream = Stream.Readable.from(generateData(generateLength, data), { objectMode: true });
const writableStream = createWriteStream(`${generateLength}-data.json`);

writableStream.write(firstLine); // start of array

+ writableStream.on('drain', () => {
+  readableStream.resume(); // restart readable stream
+ });

const counter = { input: 0 };

let isFirstChunk = true;

readableStream
  .on('data', (data) => {
    if (!isFirstChunk) writableStream.write(`${comma}${newLine}`);

    writableStream.write(JSON.stringify(data));
    counter.input++;
    console.clear();
    console.log('Input Generating...', counter.input, `/${generateLength}`);
    isFirstChunk = false;
  })
  .on('end', () => {
    writableStream.write(`${lastLine}${newLine}`);
    writableStream.end();
  });

writableStream.on('finish', () => {
  console.timeEnd('generate');
});
```

これで途中で書き込み再開し、無事1億行まで書き込みが完了します。

# タイム

上記Typescriptのコードを、bunで実行させた結果

`Time: 68m34998s`となりました。

だいたい、70分弱ぐらいで、生成中はメモリ・CPU・GPUは全然使われてない状態でした。
ジェネレーターとStreamのおかげです。

# まとめ

- 量が多いランダムデータ生成にChatGPT使えない(fakerjsを使った)
- ジェネレーター関数を使う(コールスタック解消)
- Streamを使う（メモリ解消）

# やってみて

いつもフロントエンドで書いているようなJSON量でコード書くとうまくいかない。
ジェネレーターやStreamの活きてくる場面が身をもって体感できました。
問題に当たるたびに、ChatGPTやGithub Copilotに相談して解決・解説もしてくれてペアプロはとても助かった。
エラー文とコードを提示するとエラーの内容やなんで起こったかも解説してくれました。
ググレカスは死語になりそうですね。