# 122 — 긴 보기를 연결 스레드 밖에서 2026-10-04. [020](../design/020-computing-faster.md)의 2단계(본문이 있는 보기를 보기의 스레드에서)를 잰다. 기준은 구현하기 전에 적어 커밋했다(`1d8c1f1`). 만든 것은 `2fb1da2`이고, 만들면서 정한 것은 020의 "2단계에서 정한 것"에 적었다. ## 쉬운 말로 - **계산하는 보기가 이제 다른 연결을 기다리게 하지 않는다.** 본문이 있는 보기(BFS 같은)는 보기의 스레드가 계산하고, 연결 스레드는 그동안 다른 연결을 읽고 쓴다. 연결 둘이 BFS 보기를 쉬지 않고 묻는 동안 쓰기의 최대 지연이 11 ms다. 같은 코어를 다른 프로세스와 나눌 때와 같다(1.0배, 기준 2배 이하). 전처럼 연결 스레드가 계산하면 76 ms(8.2배)다. - **보기가 연결의 자리와 상관없이 나란히 돈다.** 연결 스레드 하나에서 묻는 연결 둘이 하나의 1.95배를 받는다(기준 1.8배, 전에는 1.0배). 전에는 두 연결이 같은 연결 스레드에 떨어지면(커널이 정한다) 차례로 돌아 초당 36번, 다른 스레드면 70번이었다. 이제 늘 56–60번이다. - **짧은 보기는 그대로다.** 식 하나의 보기(`total`)는 연결 스레드가 계산하고, 그 읽기는 전과 같다(−0.2%, 기준 3% 안). - 보기를 계산하는 데 드는 CPU는 그대로다. 쓰기가 줄어드는 몫(42%만 남는다)은 다른 프로세스와 코어를 나눌 때와 같다(39.5%). ## 결과 원시 결과와 명령은 [037-view-workers](../../research/experiments/037-view-workers/)에 있다(`runs.sh`, `views.py`, `plain.py`). `views.py`는 034의 것에 쓰기 최대 지연의 중앙값과 호스트에 줄 인자(`VIEWS_SERVE_ARGS`)를 더한 것이다. 보기의 길이는 1단계 덕에 119 때(약 43 ms)보다 짧다. 그래서 견주는 쪽(`--view-workers 0`, 전처럼 연결 스레드가 계산한다)을 같은 빌드로 함께 쟀다. **방법.** `pure-serve`(연결 스레드 셋, 저장소는 RAM, 코어 0–2)에 034의 `graph.aup`(노드 2만, 간선 10만, 시드 119, `distance` 보기의 본문이 BFS). 부하 도구(코어 3)가 16연결로 점수를 10초 동안 보낸다. 셋을 번갈아: 쓰기만(writes), 연결 둘이 같은 호스트의 `distance`를 쉬지 않고 물으며(both), 그 둘이 같은 코어의 다른 호스트에 물으며(apart: CPU만 나눈다). 세 라운드씩 두 번(`runs.sh tail 1`, `tail 2`), 같은 것을 `--view-workers 0`으로 두 번(`control`). 묻는 쪽은 미리 고른 출발 64곳에서 묻고 모든 답을 모델과 견준다. 보기의 스레드는 기본(CPU 수: `taskset` 0–2이니 셋)이다. | 여섯 라운드의 중앙값 | 쓰기(초당) | 쓰기 p99 | 쓰기 최대 | 보기(초당) | |---|---:|---:|---:|---:| | writes | 120,598 | 0.28 ms | 6.0 ms | — | | both | 50,637 (42.0%) | 3.3 ms | **11.3 ms** (9.7–20.5) | 58.2 (56.4–59.6) | | apart | 47,688 (39.5%) | 3.5 ms | 11.3 ms (10.2–16.4) | 57.8 | | both, `--view-workers 0` | 47,431 (38.7%) | 14.6 ms | 75.8 ms (59–131) | 53.2 (35.7–70.5) | | apart, `--view-workers 0` | 51,301 (41.9%) | 2.0 ms | 9.2 ms | 64.0 | - 꼬리의 기준: both의 최대 지연 중앙값이 apart의 1.0배다(기준 2배 이하). `--view-workers 0`이면 8.2배다(119에서 98–188 ms 대 6–9 ms였다: 그때의 보기는 1단계 전이라 길었다). - `--view-workers 0`의 both는 라운드마다 둘 중 하나다. 두 묻는 연결이 다른 연결 스레드에 떨어지면 보기가 초당 70번 나란히 돌고 쓰기가 준다. 같은 스레드에 떨어지면 36번 차례로 돌고, 그 스레드에 붙은 쓰기 연결이 보기 하나만큼 기다린다(최대 65–131 ms). 보기의 스레드가 있으면 이 갈림이 없다(56–60번). - 쓰기가 줄어드는 몫은 둘이 같다(42.0% 대 39.5%). 보기의 계산이 코어를 쓰는 만큼이다. 쓰기의 p99가 쓰기만일 때보다 큰 것(3.3 ms)도 apart와 같다. **나란히**(`runs.sh scaling 1`, `scaling 2`): 연결 스레드 하나(`--threads 1`)에 묻는 연결 하나, 둘. 세 라운드씩 두 번. | | 하나 | 둘 | 배 | |---|---:|---:|---:| | 1 | 37.8 | 73.7 | 1.95 | | 2 | 37.8 | 73.3 | 1.94 | 여섯 라운드 모두 하나는 37.8–37.9, 둘은 72.8–74.1이다. 119에서 같은 조건은 1.0배(23.6 대 23.6)였다. **짧은 보기**(`runs.sh plain`): 점수판(`benchmarks/http/scores.aup`)에 8연결이 1초 동안 점수를 넣고, 64연결이 `GET /views/total`을 10초 동안. 전(`dde73e4`)과 후(`2fb1da2`)를 번갈아 여섯 쌍, 매번 새 호스트. | 중앙값 | 전 | 후 | 바뀜 | |---|---:|---:|---:| | 읽기(초당) | 148,867 | 148,530 | −0.2% | 여섯 쌍 모두 p50 416 µs, p99 640 µs로 같다. ### 검사 - Pure `tests/check.py`의 `server_view_workers`(새 검사): 앱 하나의 호스트(`--apps`, 연결 스레드 하나, 보기의 스레드 둘)에서 300만 번 도는 보기를 8연결이 쉬지 않고 묻는 동안, 앱의 모듈을 그 보기의 자리에 다른 보기(`other = 7`)가 있고 값이 열 배인 모듈로 바꾸고(SIGHUP), 이어서 앱을 뺀다. 모든 답이 옛 모듈의 값, 새 모듈의 값, 503 `stopped`, 404 가운데 하나다(섞인 답은 `7`로 보였을 것이다). 그 보기의 스트림이 같은 스트림에서 새 모듈의 값을 받고, 앱이 빠지면 끝난다. 값이 오기 전에 닫은 스트림 열 개가 아무것도 남기지 않는다(멈출 때 구독자 0). `--view-workers 0`이면 보기의 스레드가 없고 같은 답이다. - 같은 검사에서, 스트림의 보기를 계산하는 동안 쓰기 셋이 오면 그 뒤에 마지막 값이 온다(`id`는 늘기만 한다). 그 길(무리의 `again`)을 끄면 스트림이 마지막 값을 받지 못해 검사가 멈춘다. 앱에 `queue=2`, 보기의 스레드 하나에 8연결이 몰리면 503 `busy`가 나오고(36번), 그 뒤의 `GET`은 200이다(넘긴 일의 자리가 돌아온다). - `server_view_bodies`(64연결이 동시에 묻는 `distance` 1,280개가 모두 모델과 같다)와 `server_push`(계산하는 보기를 미는 것 포함)가 이제 보기의 스레드를 거쳐 그대로 초록이다. - Pure의 `make check`, aup와 Vega의 모든 검사가 초록이다. ## 판정 - **020의 2단계는 들어간다.** 세 통과 조건(꼬리 2배 이하: 1.0배, 나란히 1.8배 이상: 1.95배, 짧은 보기 3% 안: −0.2%)을 모두 채웠다. - 119가 남긴 두 가지(연결 스레드가 막는 꼬리, 연결의 자리에 따라 갈리는 보기의 처리량)가 없어졌다. ## 다음 - 019의 미는 보기도 보기의 스레드를 거친다(검사로만 봤다: 계산하는 보기의 스트림, 값이 오기 전에 닫힌 스트림). 구독자가 많은 계산하는 보기의 신선도는 재지 않았다. - 보기의 스레드가 다 바쁠 때 넘긴 일은 줄을 선다. 그 줄의 한도는 앱의 `queue=N`이고, 한 앱의 호스트에서는 연결 수다. 줄을 서는 동안의 지연을 다른 보기와 나누는 것(앱마다 공평하게)은 앱이 여럿일 때 잰다.