만 수의 탐정
30분짜리 측정 probe —— 라이브 2048을 headless로 플레이하며 메모리를 본다 —— 가 이 Part의 탐정담이 되었다. 메모리의 물음을 묻기 전에 더 나쁜 발견이 도착했다: 프로덕션에서 1만 수 중 9,746 수의 키 입력이, 아무도 기억하지 못하는 에러 핸들러에 삼켜져 조용히 죽고 있었다. 단서는 bounds check를 지나 「&&의 양변을 평가하던 세 번째 backend」에 다다랐고, probe가 본래 가지러 온 숫자 —— 게임의 수명은 64메가바이트 —— 로 끝났다.
두 번째 probe의 계획은 소박했다: Wasm backend는 여전히 page-lifetime 할당 —— 아무것도 해방되지 않는다 —— 인 채고, 라이브 2048은 프로젝트가 가진 최장 주행 Wasm 프로그램이다. headless로 1만 수 플레이하고, 선형 메모리를 지켜보고, 「브라우저 backend는 수마다 leak할 것」을 숫자로 바꾼다. 하네스는 작다: Node 스크립트, 목 document, 실물 DOM glue, 합성 화살표 키 이벤트. 첫 실행은 첫 키에서 떨어졌다. 메모리의 답이 무엇이든, 뭔가 다른 것이 망가져 있다 —— 게다가 프로덕션에서, 사이트 위에서, 지금 바로.
아무에게도 보이지 않는 고장
첫 교훈은 하네스 자신에게서 왔다: print import의 stub이 언어의 failure 메시지를 말없이 삼켜, 눈먼 디버깅에 한 시간을 물렸다 —— 일회용 하네스라도 failure 경로는 먼저 구현하라. 메시지가 흐르기 시작하자, 그림은 크래시보다 나쁜 것으로 날카로워졌다: 게임의 키 핸들러는 trap하고, DOM glue는 그 trap을 붙잡아, 아무도 보지 않는 console에 기록하고, 계속하고 있었다. 이 장치 아래서 게임은 죽지 않는다 —— 살아 있는 것처럼 보인 채, 대부분의 입력에 응하지 않게 될 뿐이다. 훑는 대신 세어 보니 헤드라인이 나왔다: 1만 수 중 9,746 수가 trap. 첫 세 수는 움직였고, 그 뒤는 거의 전멸. 라이브 게임은, 몇 수 이상 나아간 모든 사람에게, 공개한 그날부터 망가져 있었다 —— 그리고 공개일의 검증(headless 몇 수)은 정확히 「움직이는 몇 수」만 테스트하고 있었다. dogfood란 공개 시에 검증하는 것이 아니라, 계속 돌리는 것이었다.
세 번째 backend
기호화한 스택 트레이스(빌드 플래그 하나)는 게임의 막힘 판정 —— 보드를 주사하며 「아직 둘 수 있는
수가 있는가?」 묻는 루틴 —— 을 가리켰다. 그 안쪽의 테스트는 프로그래밍에서 가장 흔한 이디엄이다:
r < 3 && bget b (i + 4) == v —— index를 guard하고 나서 읽는다. guard는 일하고 있었다. 읽기는
그래도 일어나고 있었다. interpreter는 &&를 short-circuit한다. C backend는 C 자신에게서
short-circuit을 상속한다. Wasm backend는 양변을 평가하고 있었다 —— 그 &&는 strict한 비트
명령으로 컴파일되어 있었다 —— 그리고 LLVM backend도 같은 죄임이 판명됐다. 「MVP 서브셋에 effect는
없으므로 eager 평가는 관측적으로 등가」라 주장하는 코멘트의 그늘에서. 그 코멘트는 한때 참이었다.
trap하는 배열 읽기는 effect다. 수정은 두 backend의 &&/||를 기존 if/else emission으로 내리는
것이었고, bounds-guard 딸린 읽기는 이제 어디서나 안전하다 —— short-circuit의 의미를 배운 세 번째와
네 번째 backend이자, 「코멘트 속 의미론의 주장은 코드보다 나쁘게 늙는다」에 대한 경계이기도 하다.
probe가 가지러 온 숫자
게임이 실제로 놀 수 있게 되자, 원래의 물음이 답을 얻었다. 기분 좋을 만큼 정확한 답을. 남은 trap은 전부 move 7,764에서 시작했고, 앨러케이터의 bump pointer는 모듈의 64메가바이트 메모리 천장에 딱 서 있었다: 게임은 한 수마다 약 8.4킬로바이트의 회수되지 않는 메모리 —— 보드 스냅숏, 렌더 문자열 —— 를 태우고, page-lifetime 모델이 페이지를 다 쓰면 죽는다. 열심인 플레이어라면 한 시간 안에 탭을 죽일 수 있다. 이것은 게임의 버그가 아니다. 회수를 갖지 않은 backend의 정직한 비용이, 유효숫자 세 자리로 측정되었을 뿐이다. 측정은 forcing number의 일을 했다: 「메모리 모델의 성과를 Wasm에 이식한다」를 「언젠가」에서 대기열의 선두로 옮겼고, 수용 테스트까지 다 쓰여 있었다 —— 이 같은 probe가, 평평한 선을 기대하며 기다리고 있다. 다음 회가 그것을 돌린다.