
# 벤치마크

벤치마크 하네스는 안정적인 JSON와 사람이 읽기 쉬운 보고서를 작성합니다. 이 도구는 `bench/`에 있으며, [storh 저장소](https://github.com/inline0/storh)에 포함되어 있습니다. Composer 패키지에는 0.5.2 이후 포함되지 않았으므로, 아래 `composer bench*` 명령은 복제본에서 `composer install`한 후에만 실행됩니다:

```bash
composer bench
composer bench -- --dataset=100000 --engine=doc
composer bench -- --dataset=100000 --engine=log
composer bench -- --dataset=100000 --engine=queue
composer bench -- --dataset=100000 --engine=cache --cache-validation=trust
composer bench -- --dataset=100000 --engine=filter
composer bench -- --dataset=1000000 --engine=uuid
composer bench:jit -- --dataset=1000000 --engine=uuid
composer bench:repeat -- --dataset=100000 --engine=doc --repeat=5
composer bench:repeat -- --dataset=1000000 --engine=filter --repeat=5 --memory-limit=512M
composer bench:range -- --datasets=1000,10000,50000,100000
composer bench:range -- --datasets=100000 --engines=cache --memory-limit=512M
composer bench:compare build/bench-main.json build/bench-current.json
composer bench:gate -- build/bench-main.json build/bench-current.json --threshold=10 --metric=doc.put --metric=log.stream
composer bench:ci
```

## 기준 결과

50,000개의 레코드를 엔진별로 처리한 하네스 전체 실행 3회의 중앙값입니다. Apple M1 Pro(16 GB, macOS 26.5, APFS)에서 PHP 8.5 CLI를 사용했으며 opcache와 JIT는 껐습니다.

```bash
composer bench:repeat -- --dataset=50000 --engine=all --repeat=3 --memory-limit=512M
```

storh에서 저장됐다고 보고하기 전에 모든 쓰기를 flush 및 fsync하므로 쓰기 속도는 파일 시스템에 좌우됩니다. 읽기는 OS 페이지 캐시에서 실행됩니다. 속도는 중앙값에서 산출했습니다. 사용자의 하드웨어에서 측정하려면 리포지토리를 클론해 `composer bench`을 실행하세요.

| DocStore, 레코드 50k개                       | 중앙값          |
| ---------------------------------------------- | --------------- |
| `put()`, 레코드당 내구성 파일 1개               | 3.3k 레코드/s  |
| `putStream()` 대량 수집                         | 3.7k 레코드/s  |
| `importJsonl()`                                         | 3.6k 레코드/s  |
| `get()` 지점 읽기, STAT 검증                   | 5.6 µs          |
| 기존 store 다시 열기                           | 0.26 ms         |
| 인덱스 동등성 쿼리, `limit(100)`                    | 1.1 ms          |
| 컬렉션 전체 인덱스 `count()`                      | 41 µs           |
| 인덱스 빌드, 동등성 필드 2개 + 범위 필드 1개     | 52k 레코드/s   |
| STAT 재검증을 포함한 전체 `stream()`                | 103k 레코드/s  |
| `exportJsonl()`                                         | 104k 레코드/s  |

| SegmentedLog, 레코드 50k개, 세그먼트 16 KB       | 중앙값          |
| ---------------------------------------------- | --------------- |
| `put()`, 추가마다 fsync                         | 초당 16k회 추가 |
| `appendStream()` 대량 수집                         | 56k 레코드/s   |
| 커서 읽기, 중간 지점부터 레코드 100개          | 2.1 ms          |
| 시간 범위 읽기                                 | 1.1 ms          |
| 동등성 `count()`                                   | 11 µs           |
| 봉인된 모든 세그먼트 `compact()`                     | 69k 레코드/s   |
| 잘린 꼬리 복구를 포함해 다시 열기                 | 218k 레코드/s  |

벤치마크에서는 세그먼트 롤오버를 확인하기 위해 16 KB마다 세그먼트를 봉인합니다. 기본 세그먼트 크기는 1 MiB입니다.

| 큐, 작업 50k개                                 | 중앙값          |
| ---------------------------------------------- | --------------- |
| `enqueue()`, 이벤트마다 fsync                   | 22k 작업/s     |
| `claim()`                                        | 23k 작업/s     |
| `complete()`                                        | 23k 작업/s     |
| `enqueueMany()`                                        | 209k 작업/s    |
| `claimMany()`                                        | 467k 작업/s    |
| `completeMany()`                                        | 512k 작업/s    |

| SQL Mirror, SQLite, 레코드 50k개             | 중앙값          |
| ---------------------------------------------- | --------------- |
| 초기 `push()`                                     | 61k 행/s       |
| 변경 사항이 없을 때 `push()`                     | 86k 레코드/s   |
| `flush()`, 100 ID                               | 7.1 ms          |
| 미러 전체 인덱스 SQL `COUNT`                   | 6.9 ms          |
| `rebuild()`                                         | 65k 행/s       |
| `pull()` 복구, 레코드당 내구성 파일 1개             | 3.2k 레코드/s  |

| 마이크로                                        | 중앙값          |
| ---------------------------------------------- | --------------- |
| 캐시된 `get()`, 콜드 후 웜(MemoryCache, STAT)       | 83 µs / 6.3 µs |
| UUIDv7 생성                                     | 1.3 µs          |
| UUIDv7 검증                                     | 0.30 µs         |
| 메모리 내 조건자 필터링                          | 4.4M 행/s     |

대상 범위:

- DocStore put, 스트리밍 put, JSONL 내보내기/가져오기, get, delete, stream 및 인덱스 빌드
- 동등성 인덱스, 복합 인덱스, 범위 인덱스, 오름차순 및 내림차순 정렬 범위, 인덱스 기반 카운트, ID 조회 및 비인덱스 DocStore 쿼리
- SegmentedLog append, 스트리밍 append, 커서 읽기, 시간 범위 읽기, 압축된 시간 범위 읽기, QueryBuilder 커서 및 ID 조회, 카운트, 일괄 append 카운트, 통계, 압축
- Queue enqueue, 일괄 enqueue, claim, 일괄 claim, complete, 일괄 complete, requeue
- 메모리 내 행에 대한 RecordQuery 및 QueryCondition 조건자 필터링
- UUIDv7 단조 증가 생성, 타임스탬프 분산 생성, 검증 및 타임스탬프 추출
- 끝부분이 잘린 줄 복구
- 해시, stat 또는 신뢰 기반 검증을 사용하는 콜드 및 웜 캐시 읽기

기본 출력 경로:

```text
build/bench-current.json
```

범위 실행은 데이터 세트와 엔진별로 JSON 파일 하나를
`build/bench-ranges/`에 씁니다.

`bench:gate`은 벤치마크 JSON 파일 두 개를 비교하고, 추적하는 지표 중 하나라도 설정된 임계값보다 느리면 0이 아닌 상태 코드로 종료합니다. `--metric` 값이 전달되지 않으면 기준 파일에 있는 모든 지표를 검사합니다.

`bench:ci`은 GitHub Actions 스모크 회귀 검사 게이트입니다. 모든 엔진에서 1k 벤치마크를 실행하고, 선택한 쓰기/읽기/캐시/UUID 지표를 추적 중인 `bench/baselines/ci-1k-all.json` 기준과 비교합니다. 일반적인 실행 환경의 변동에도 실패하지 않도록 기본 임계값을 넉넉하게 설정하면서도 큰 성능 회귀는 감지합니다.

`bench:jit`은 같은 하네스를 CLI opcache와 추적 JIT를 활성화한 상태로 실행하며, 벤치마크 프로세스에서 Xdebug를 비활성화해 PHP이 실제로 JIT를 켤 수 있게 합니다. 생성된 JSON에는 해당 실행에서 opcache와 JIT가 활성 상태였는지 보여 주는 런타임 플래그가 포함됩니다.
