aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-04 11:07:55 +0100
committereunhhu <[email protected]>2026-10-04 11:07:55 +0100
commit52e51e4231f2e119c97826ae6d6e2f2af0da0c58 (patch)
tree0f17ced79a1881bd2b67fbaa9e7ffb823f54b984
parent420b749742badd26daf2474e9f157af17be2907f (diff)
downloadpure-52e51e4231f2e119c97826ae6d6e2f2af0da0c58.tar.gz
pure-52e51e4231f2e119c97826ae6d6e2f2af0da0c58.zip
Say what apps that change on a running host showed (017 stage 3, research 115)
Changing an app in place keeps a million items' app answering but for 1.12 ms (a host restart: 150 ms; stopping and starting it again from its store, as first built, 150.4 ms), 171.5 ms when its records change shape (332 ms). Another app's p99 stays 0.97 times as it was while one changes every 0.2 s; a host of one app is as it was (TPC-C +0.7%, instructions at most +0.02%); each module change leaves 19 KB. 030 holds the windows of both builds, the isolation, the pairs, the counts and the memory.
-rw-r--r--CHECKPOINT.md6
-rw-r--r--docs/design/017-apps-on-one-host.md30
-rw-r--r--docs/research/115-apps-that-change.md115
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl11
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl11
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-load.txt12
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl2
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/isolation.py86
-rw-r--r--research/experiments/030-app-lifecycle/items-wide.aup29
-rw-r--r--research/experiments/030-app-lifecycle/memory.py79
-rw-r--r--research/experiments/030-app-lifecycle/pairs.py45
-rwxr-xr-xresearch/experiments/030-app-lifecycle/runs.sh35
-rwxr-xr-xresearch/experiments/030-app-lifecycle/window.py33
46 files changed, 747 insertions, 23 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 3c53700..d179039 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -18,7 +18,7 @@
| HTTP 서비스 | 보기가 GET, 입력이 POST다(JSON 본문과 그 안의 그룹 객체를 제자리에서 읽는다, 라우터 코드 없음). 앱의 `send`는 이벤트와 함께 저장되고, `--send 출력=URL`로 다른 앱이나 HTTP 끝점에 순서대로 묶어 전해지며, 받는 앱은 번호마다 한 번 적용한다(서버를 죽였다 살려도 정확히 한 번). `--seal 출력=공개키`면 받는 노드의 키로 봉인해(HPKE, 전달선마다 세션 하나, 보내는 노드의 키로 인증) 사이의 중계가 읽지도 바꾸지도 못하고, 받는 노드는 `--sender 이름=키`로 보낸 이를 가린다(봉인 전달 462k/s, 봉인 없이 468k/s). 답은 저장 뒤에 나간다. 상태를 가진 앱은 소유자 스레드 하나가 입력을 적용하고, 연결 스레드는 마지막 저장의 스냅숏으로 보기를 답한다(쓰기가 동기화하는 동안에도 읽기는 기다리지 않는다) | 008 HTTP 호스트, [077](docs/research/077-apps-as-http-services.md), [080](docs/research/080-snapshot-reads.md), [082](docs/research/082-json-in-place.md), [083](docs/research/083-delivery-over-http.md), [086](docs/research/086-sealed-sends.md), [088](docs/research/088-sealed-sessions.md) |
| 성능(Pi 5) | 2026-09-27, 같은 기기에서 axum·drogon과 함께 쟀다. 문서 서버(한 스레드): 64 B GET 139k/s(CPU 1초당 140k; axum 127k·63k, drogon 147k·63k), 1 MiB GET 2,362/s(axum 1,304, drogon 754), 충돌 안전 PUT은 axum-atomic의 5.1–5.8배. 앱 서비스: 내구성 있는 쓰기 64연결 21.3k/s(axum·drogon 내구 약 200/s), 합계 GET 126k/s·두 스레드 153k(axum 136k, drogon 151k), 20행 페이지 두 스레드 148k(axum 93k, drogon 138k), 쓰기와 섞인 읽기 119k/s(axum·drogon 내구 1.1–1.4k/s). 유휴 RSS 2.3 MiB(axum 2.8, drogon 10.9), 최대 3.2–5.8 MiB(axum 6.6–9.7, drogon 11.9–78.8) | [075](docs/research/075-http-host-against-axum.md), [077](docs/research/077-apps-as-http-services.md), [080](docs/research/080-snapshot-reads.md), [081](docs/research/081-tail-records.md), [087](docs/research/087-small-reads-and-idle-memory.md) |
| 노드 사이 전달 | 연결 없는 링크가 기본이다(`--send 출력=pure://호스트:포트/입력`, 2026-09-29 채택). 두 노드의 키로 첫 패킷부터 봉인해 보내고 핸드셰이크가 없다. 어느 쪽을 kill -9 해도, 받는 노드의 저장소를 다른 주소로 옮겨도 다음 패킷으로 이어지며 잃거나 겹친 기록이 없다(기기 1만 개가 초당 2,000건을 보내는 중에 저장이 멈춘 100 ms 칸 0). 쉬는 링크는 받는 노드 메모리 132 B(quinn 연결의 1/289). 에폭마다 키를 바꿔 전방 비밀성을 얻는다. 전달당 CPU는 1 KiB 이상에서 quinn·ngtcp2보다 25 % 넘게 적고 61 B에서 비긴다. 손실·재정렬·병목 경로(M6)에서 재정렬 아래 quinn·ngtcp2보다 많이 나르고, 무작위 손실 아래 같으며, 깨끗한 병목에서 틀의 바이트로 3–5 % 적다. 명세가 있고, 명세만 보고 쓴 파이썬 받는 쪽이 노드와 맞붙는다. 받는 입력은 필드를 이름으로 받는다(순서가 달라도, 받는 쪽 필드가 더 있어도 되고, 보낸 필드를 버리지는 않는다). 그래서 두 노드가 함께 쓰는 보드를 한 노드씩 올려도 멈추지 않고, 기다리는 보내기도 새 필드로 옮겨진다([105](docs/research/105-growing-a-shared-board.md), [014](docs/design/014-fields-by-name.md)) | [설계 011](docs/design/011-pure-transport.md), [명세 012](docs/design/012-link-protocol.md), [093](docs/research/093-connectionless-link.md)–[102](docs/research/102-adoption-and-spec.md) |
-| 여러 앱 | 한 호스트가 앱 여럿을 품는다(`pure-serve --apps`, 경로는 `/앱/…`). 앱마다 스레드를 두지 않고 정해진 실행 스레드가 차례로 돌리며, 앱마다 연결이 모이는 집 연결 스레드가 있다. 앱마다 실행량(쓴 그대로의 명령 수, 넘으면 422 `work`)·대기열(503 `busy`)·메모리 예산이 있고, 모듈이나 저장소를 열지 못한 앱은 그 앱만 멈춘다. 앱 하나에 120 KiB(프로세스 하나의 PSS의 9%), 앱 100개에도 스레드 셋. 한 앱이 몰릴 때 다른 앱의 p99는 앱마다 프로세스일 때의 0.5배, 둘 다 바쁠 때 처리량과 p99는 같다. 앱마다 보냄을 배선하고(`send=`), 링크 포트 하나가 모든 앱의 입력을 `앱/입력`으로 받는다. 같은 호스트의 앱끼리는 소켓 없이 건네 링크의 두 배(초당 114만 건)를 CPU 40%로 나른다. 링크와 전달선은 프로세스의 보내는 루프 하나가 보낸다(링크와 전달선 스무 개에 스레드 24개가 5개, 링크 +63%, 전달선 같음) | [017](docs/design/017-apps-on-one-host.md), [111](docs/research/111-apps-on-one-host.md), [112](docs/research/112-sending-between-apps.md), [113](docs/research/113-one-sending-loop.md) |
+| 여러 앱 | 한 호스트가 앱 여럿을 품는다(`pure-serve --apps`, 경로는 `/앱/…`). 앱마다 스레드를 두지 않고 정해진 실행 스레드가 차례로 돌리며, 앱마다 연결이 모이는 집 연결 스레드가 있다. 앱마다 실행량(쓴 그대로의 명령 수, 넘으면 422 `work`)·대기열(503 `busy`)·메모리 예산이 있고, 모듈이나 저장소를 열지 못한 앱은 그 앱만 멈춘다. 앱 하나에 120 KiB(프로세스 하나의 PSS의 9%), 앱 100개에도 스레드 넷(연결 하나, 실행 둘, 구성 파일을 읽는 하나). 한 앱이 몰릴 때 다른 앱의 p99는 앱마다 프로세스일 때의 0.5배, 둘 다 바쁠 때 처리량과 p99는 같다. 앱마다 보냄을 배선하고(`send=`), 링크 포트 하나가 모든 앱의 입력을 `앱/입력`으로 받는다. 같은 호스트의 앱끼리는 소켓 없이 건네 링크의 두 배(초당 114만 건)를 CPU 40%로 나른다. 링크와 전달선은 프로세스의 보내는 루프 하나가 보낸다(링크와 전달선 스무 개에 스레드 24개가 5개, 링크 +63%, 전달선 같음). 달리는 호스트가 SIGHUP에 구성 파일을 다시 읽어 앱을 더하고, 멈추고, 그 자리에서 바꾼다(상태는 메모리에서 이름으로 옮긴다: 100만 항목의 앱이 1.1 ms만 쉬고, 다른 앱의 p99는 그대로) | [017](docs/design/017-apps-on-one-host.md), [111](docs/research/111-apps-on-one-host.md), [112](docs/research/112-sending-between-apps.md), [113](docs/research/113-one-sending-loop.md), [115](docs/research/115-apps-that-change.md) |
| 메모리 한도 | 앱마다 상태의 예산이 있다(`--memory`). 목록의 항목, 텍스트, outbox, 들은 번호를 저장 형식의 바이트로 센다(어느 기기에서나 같다). 예산을 넘기며 늘리는 사건은 거절하고(503 `memory`) 줄이는 사건은 받는다. 받는 쪽이 늦으면 outbox가 차서 클라이언트까지 역압이 가고, 가득 찬 받는 쪽은 링크로 알려 보내는 쪽이 다시 보내지 않는다. 답(`--reply`)은 자기 보내기의 자리를 쓴다. 상태 4 MB를 준 노드는 부하 아래 상주 9.6–9.8 MiB에 멈춘다(예산 없이 30초에 45.9 MiB). 오래 붙든 스냅숏은 그때 상태 한 벌만 쥔다(전: 30초에 678 MiB) | [설계 013](docs/design/013-memory-budget.md), [104](docs/research/104-where-memory-goes.md) |
| 표면과 메신저 | 사람을 위한 표면이 노드 안에서 돈다(`pure-serve --surface`): 마지막 저장의 스냅숏을 제자리에서 읽고, 친 키와 글자는 HTTP 요청과 같은 줄로 소유자에게 간다. Vega의 디스플레이·픽셀이 그 표면이다. 두 노드의 메신저(Vega `examples/messenger.aup`)는 서로에게 봉인·인증해 보내고, 한 줄이 상대 노드에 보이기까지 RAM 저장소에서 p50 0.79 ms·p99 1.1 ms, SD 카드에서 p50 3.5 ms(꼬리는 SD의 동기화) | [설계 010](docs/design/010-surfaces.md), [089](docs/research/089-messenger.md) |
| 표 크기의 상태 | 목록이 영속 B+트리다(변경·키 탐색·저장본 위치 O(log n), 스냅숏 O(1), 1,600만 항목, 런타임 상태 1 GiB). pgbench의 TPC-B 같은 이체(세 표의 행과 이력, 한 트랜잭션)가 10만 계좌에서 RAM 24.5k/s(전: 3만 계좌 3.2k/s), SD 카드 내구 1,339/s(SQLite WAL FULL 84–153/s) | [090](docs/research/090-tables.md) |
@@ -32,8 +32,8 @@
+9%, 상주 −10%, [107](docs/research/107-snapshot-nodes-reused.md)). 남은 가장 큰 몫은 항목 찾기다. 키로 나눈 소유자는 그다음에 본다. 대신 압축한 파일이 한 주기의 0을 미리 가져 커밋이 새
블록을 받지 않는다(저장소 커밋 +21%, 저널 커밋 −59%, [085](docs/research/085-cycle-sized-files.md)).
-남은 것: macOS의 호스트(`pure-serve`는 리눅스의 것), 달리는 호스트의 앱을 더하고 바꾸기(017의 3단계), 압축을 없애는 제자리 재사용 저장소(085 다음), 응답을 기다리는 출력의 묶음 전달(083), 앱
-라우트의 인증·TLS(문서 피어의 TLS는 있다), aup·Vega의 배포 묶음, 캐시보다 큰 목록을 찾는 걸음(109: 미리 가져오기로는 줄지
+남은 것: macOS의 호스트(`pure-serve`는 리눅스의 것), 바뀐 모듈을 놓기(바꿀 때마다 19 KB, 115), 압축을 없애는 제자리 재사용 저장소(085 다음), 응답을 기다리는 출력의 묶음 전달(083), 앱
+라우트의 인증·TLS(017의 3b, 문서 피어의 TLS는 있다), aup·Vega의 배포 묶음, 캐시보다 큰 목록을 찾는 걸음(109: 미리 가져오기로는 줄지
않았다. 한 사건의 조회를 겹치거나 구조를 바꿔야 한다).
## 공개할 작업물과 완료 증거
diff --git a/docs/design/017-apps-on-one-host.md b/docs/design/017-apps-on-one-host.md
index f9ff765..3f2ce2b 100644
--- a/docs/design/017-apps-on-one-host.md
+++ b/docs/design/017-apps-on-one-host.md
@@ -1,7 +1,8 @@
# 017 — 한 호스트의 여러 앱: 차례, 예산, 실패 경계
작성일: 2026-10-01. 상태: 1·2단계와 2b 구현, 검사로 고정하고 쟀다([111](../research/111-apps-on-one-host.md),
-[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계는 착수 기준을 적었다(2026-10-04).
+[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계(수명) 구현, 검사로 고정하고
+쟀다([115](../research/115-apps-that-change.md)). 3b(경로의 인증)는 설계 전.
[외부 리뷰 004](../outside-feedback/feedback-004.md)의 "구현 방향에 대한 평가" 셋째에서 시작한다. 작은 OS의 운영 모델(
[007](007-execution-domains.md))을 Pure가 실제로 갖게 하는 단계다.
@@ -238,17 +239,20 @@ counter apps/count.pmod stores/counter
- 새 줄의 앱은 시작한다. 1단계의 시작과 같다(모듈 검증, 저장소, 배선, 실패하면 그 앱만 멈추고 까닭을 남긴다).
- 없어진 줄의 앱은 멈춘다(아래의 순서). 그 뒤로 그 경로는 404 `no_app`이다.
-- 바뀐 줄의 앱은 멈추고 새 줄로 다시 시작한다. 모듈 파일의 내용, 저장소, 예산, 배선 가운데 하나라도 바뀐 줄이다. 상태는
- 저장소에서 이름으로 이어받는다(지금 다른 모듈로 다시 띄울 때와 같다, `server_upgrade`).
+- 바뀐 줄의 앱은 그 자리에서 바뀐다. 모듈 파일의 내용, 예산, 배선 가운데 하나라도 바뀐 줄이다. 받은 일을 다 적용하고 저장한
+ 뒤, 메모리의 상태를 그대로 두고 모듈을 바꾼다(`pure_app_replace`: 상태와 outbox의 보냄을 이름으로 옮긴다). 저장소는 열린
+ 채이고, 출구만 새 모듈로 다시 만든다. 새 모듈에 상태가 들어가지 않으면 그 앱은 멈추고 까닭을 남긴다.
+- 저장소가 바뀐 줄의 앱, 저장이 불확실해진 앱은 멈추고 저장소에서 다시 연다(상태는 이름으로 이어받는다, `server_upgrade`).
- 그대로인 줄의 앱은 건드리지 않는다. 멈춰 있거나(시작하지 못했거나) 불확실해진 앱만 다시 시작한다. 저장소에서 다시 연다.
-- 다 맞추면 시작할 때와 같은 꼴의 줄 하나를 stdout에 낸다. 앱마다 상태와 까닭, 그리고 이번에 한 일(시작, 멈춤, 다시 시작)이다.
+- 다 맞추면 시작할 때와 같은 꼴의 줄 하나를 stdout에 낸다(`{"reading":N,"apps":[…]}`). 앱마다 상태와 까닭, 그리고 이번에 한
+ 일이다(kept, changed, restarted, started, removed).
- 파일을 읽지 못하거나 틀린 줄이 있으면 아무것도 바꾸지 않고 까닭을 낸다.
HTTP로 앱을 더하는 길을 두지 않는 까닭은 인증이다. 그 길은 경로의 인증(3b)을 먼저 요구한다. 신호는 프로세스의 주인만 보낼 수 있고,
파일은 호스트의 구성을 그대로 남긴다(다시 띄워도 같은 모습이다). 다시 읽기는 호스트의 스레드 하나(`pure-apps`)가 한다. 연결
스레드와 실행 스레드는 기다리지 않는다.
-**멈추는 순서.** 앱 하나를 멈추는 것은 이렇다.
+**멈추는 순서.** 앱 하나를 멈추는 것은 이렇다(그 자리에서 바꿀 때는 1–3까지 하고, 모듈을 바꾼 뒤 이어 간다).
1. 새 일을 받지 않는다. 그 앱의 경로는 503 `stopped`다. 링크와 안쪽 출구의 기록은 받지 않는다(보내는 쪽이 쥐고 다시 보낸다).
2. 소유자가 이미 받은 일을 다 적용하고, 저장하고, 답한다.
@@ -279,11 +283,25 @@ HTTP로 앱을 더하는 길을 두지 않는 까닭은 인증이다. 그 길은
**하지 않는 것.**
-- 끊김 없는 교체(새 모듈을 옆에서 띄우고 넘기기). 멈추고 다시 여는 동안 그 앱은 503을 답한다. 그 시간을 잰다.
+- 끊김 없는 교체(새 모듈을 옆에서 띄우고 넘기기). 바꾸는 동안 그 앱은 503을 답한다. 그 자리에서 바꾸면 1 ms 남짓이다(기록의
+ 모양이 바뀌는 큰 목록은 목록을 다시 쓰는 동안).
- HTTP로 앱을 더하고 바꾸는 길. 경로의 인증(3b) 뒤에 따로 정한다.
**3b: 경로의 인증.** 앱의 경로를 누가 부를 수 있는지다(CHECKPOINT의 남은 것). 3단계의 수명과 따로 설계한다.
+### 3단계에서 알게 된 것
+
+- **멈추고 다시 열면 다시 읽기가 전부였다.** 처음 구현(`12878cf`)은 바뀐 앱을 멈추고 저장소에서 다시 열었다. 항목 100만 개의
+ 앱이 150.6 ms를 답하지 못해 호스트를 다시 띄우는 것(149.7 ms)과 같았다. 그 가운데 142 ms가 저장소를 다시 읽는 일이었다.
+ 저장소가 그대로인 앱을 그 자리에서 바꾸자(`7daf406`) 1.12 ms가 됐다(1,024개 1.06 ms). 기록의 모양이 바뀌는 모듈은 목록을 새
+ 모양으로 다시 쓰느라 171.5 ms다(다시 띄우면 332 ms).
+- **결과.** 한 앱을 거듭 바꾸는 동안 다른 앱의 p99는 0.97배, 앱 하나의 호스트는 TPC-C +0.7%와 명령 수 +0.02% 이하, 모듈을 바꿀
+ 때마다 19 KB가 남는다. 파일을 읽는 스레드가 하나 늘었다(앱 100개에 스레드 넷). 커밋: Pure `14c75d0`(해석한 모듈과 앱의 자리),
+ `1d892e2`(링크 스트림의 입력을 다시 찾기), `12878cf`(SIGHUP과 멈추고 다시 열기), `7daf406`(그 자리에서 바꾸기), `f5e9db5`와
+ 그 뒤(측정).
+- **검사가 찾은 것.** 보내는 앱이 멈추는 동안 받는 앱의 차례가 놓인 출구를 읽던 자리, 시작하기 전의 앱의 락을 다른 앱의 출구가
+ 쓰던 것. 둘 다 바꾸기 한가운데의 보내는 앱과 받는 앱을 함께 바꿔 보는 검사로 드러났다.
+
## 3단계의 착수 기준
- **바꿀 결정:** 달리는 호스트의 앱을 SIGHUP과 구성 파일로 더하고, 멈추고, 바꿀지다. 아래를 채우지 못하면 앱을 바꿀 때 호스트를
diff --git a/docs/research/115-apps-that-change.md b/docs/research/115-apps-that-change.md
new file mode 100644
index 0000000..a76f9dd
--- /dev/null
+++ b/docs/research/115-apps-that-change.md
@@ -0,0 +1,115 @@
+# 115 — 달리는 호스트의 앱을 더하고, 멈추고, 바꾸기
+
+2026-10-04. [017](../design/017-apps-on-one-host.md)의 3단계를 잰다. 기준은 구현하기 전에 017에 적어 커밋했다(`e4c7f7e`).
+
+- 바꿀 결정: 달리는 호스트의 앱을 SIGHUP과 구성 파일로 더하고, 멈추고, 바꿀지. 채우지 못하면 앱을 바꿀 때 호스트를 다시 띄운다.
+- 통과 조건(RAM의 Pi 5):
+ - 바꾸는 앱이 답하지 못하는 시간이 호스트를 다시 띄울 때보다 짧다(항목 1,000개와 100만 개).
+ - 한 앱을 거듭 바꾸는 동안 다른 앱(한 연결)의 p99가 바꾸지 않을 때의 1.5배를 넘지 않는다.
+ - 앱 하나의 호스트는 그대로다(TPC-C와 은행 질의의 중앙값이 3% 넘게 느려지지 않고, 명령 수가 1% 넘게 늘지 않는다).
+ - 바꿀 때마다 남는 메모리를 밝힌다.
+
+## 쉬운 말로
+
+- **통과했다.** 호스트가 SIGHUP에 `--apps` 파일을 다시 읽고, 줄이 없어진 앱은 멈추고, 새 줄의 앱은 시작하고, 바뀐 줄의 앱은 그
+ 자리에서 바꾼다. 항목 100만 개의 앱이 모듈을 바꾸는 동안 답하지 못한 시간은 1.1 ms다(호스트를 다시 띄우면 150 ms). 그동안
+ 다른 앱의 p99는 그대로다(0.97배). 앱 하나의 호스트도 그대로다.
+- **처음 구현은 큰 앱에서 기준을 넘지 못했다.** 앱을 멈추고 저장소에서 다시 여는 것으로 바꿨다. 100만 항목의 앱은 150.6 ms를
+ 답하지 못했다. 호스트를 다시 띄우는 것(149.7 ms)과 같았다. 시간을 나눠 보니 거의 전부(142 ms)가 저장소를 다시 읽는
+ 데(`pure_app_start`) 들었다. 호스트를 다시 띄울 때도 같은 일을 하니 짧아질 수 없었다.
+- **고친 것은 다시 읽지 않는 것이다.** 저장소가 그대로인 앱은 받은 일을 다 적용하고 저장한 뒤, 메모리의 상태를 그대로 두고
+ 모듈만 바꾼다(코어의 `pure_app_replace`: 상태와 outbox의 보냄을 이름으로 옮긴다). 출구만 새 모듈로 다시 만든다. 그러자 크기와
+ 상관없이 1 ms 남짓이 됐다. 기록의 모양이 바뀌는 모듈(필드가 하나 늘어난 기록)은 목록을 새 모양으로 다시 만들고 이미지를 다시
+ 써야 해서 172 ms가 들었다. 호스트를 다시 띄우면 다시 읽기에 그 일까지 더해 332 ms다.
+- **모듈을 바꿀 때마다 19 KB가 남는다.** 바뀐 모듈을 놓지 않는다. 그 모듈로 해석된 요청과 스냅숏이 아직 그것을 가리킬 수 있기
+ 때문이다. 예산이나 배선만 바뀌면 남는 것이 없다.
+
+## 결과
+
+원시 결과와 명령은 [030-app-lifecycle](../../research/experiments/030-app-lifecycle/)에 있다(`runs.sh`의 단계 `build`, `window`,
+`isolation`, `pairs`, `counts`, `memory`; `window.py`, `isolation.py`, `memory.py`, `pairs.py`; 앱 `items.aup`과 그 다음 판
+`items-next.aup`(보기 하나 더), `items-wide.aup`(기록에 필드 하나 더)).
+
+**답하지 못하는 시간**(`window.py`). 호스트(`--apps`, 저장소는 RAM, 코어 0–2)에 앱 하나가 항목을 든다(사건 하나가 목록을 두 배로
+늘려 1,024개나 1,048,576개). 클라이언트(코어 3)가 한 연결로 쓰기를 쉬지 않고 보내고, 답을 받은 두 쓰기 사이의 가장 긴 틈을 잰다.
+바꾸기는 줄이 다른 모듈 파일을 가리키게 하고 SIGHUP을 보내는 것이다(두 모듈을 번갈아). 다시 띄우기는 같은 변경을 전처럼 하는
+것이다(줄을 바꾸고 SIGTERM, 다시 시작). 1,024개는 바꾸기 20번과 다시 띄우기 10번, 1,048,576개는 10번과 5번의 중앙값이다.
+
+| 앱, 바뀌는 것 | 멈추고 다시 열기(`12878cf`) | 그 자리에서(`7daf406`) | 호스트를 다시 띄우기 |
+|---|---:|---:|---:|
+| 1,024개, 보기 하나 더 | 1.52 ms | **1.06 ms** | 2.42 ms |
+| 1,024개, 기록에 필드 하나 더 | 1.58 ms | **1.31 ms** | 2.51–2.64 ms |
+| 1,048,576개, 보기 하나 더 | 150.42 ms | **1.12 ms** | 149.63–149.94 ms |
+| 1,048,576개, 기록에 필드 하나 더 | 337.33 ms | **171.54 ms** | 329.85–332.42 ms |
+
+다시 띄우기의 범위는 두 빌드에서 잰 값이다. 처음 잰 것(`f5e9db5`)은 다시 띄울 때 모듈을 바꾸지 않았다(1,024개 1.59 대 3.05 ms,
+100만 개 150.64 대 149.73 ms). 비교가 같은 변경이 되도록 고쳐 다시 쟀다.
+
+멈추고 다시 열 때 100만 항목의 시간을 나누면, 멈추기 7–8 ms(상태를 놓기), 시작 142 ms(저장소를 다시 읽기)다. 호스트를 다시
+띄울 때의 시작은 170 ms였다. 그 자리에서 바꾸면 이 둘이 없다. 기록의 모양이 바뀌면 목록 100만 항목을 새 모양으로 다시 만들고
+이미지를 쓰는 데 약 170 ms가 든다. 다시 띄울 때도 같은 일을 다시 읽기에 더해서 한다.
+
+**다른 앱의 꼬리**(`isolation.py`). 같은 호스트(연결 스레드 둘, 실행 스레드 둘)에 100만 항목의 앱 A와 점수판 B. B에 한 연결의
+점수를 10초 보내는 동안 A를 그대로 두거나 바꾼다. 바꾸는 쪽은 보기 하나 더인 모듈로 0.2초마다(판마다 50번), 기록에 필드 하나
+더인 모듈로 0.4초마다(판마다 18번, 한 번에 약 170 ms의 일)다. 다섯 라운드, 순서를 번갈아.
+
+| B의 p99 중앙값 | 그대로 | 바꾸는 동안 | 비 |
+|---|---:|---:|---:|
+| 보기 하나 더, 0.2초마다 | 120 µs | 116 µs | 0.97 |
+| 기록에 필드 하나 더, 0.4초마다 | 120 µs | 116 µs | 0.97 |
+
+B는 5xx를 하나도 받지 않았다. 바꾸는 일은 파일을 읽는 스레드(`pure-apps`)에서 하고, 실행 스레드는 그 앱의 차례만 쉰다.
+
+**앱 하나의 호스트**(`runs.sh pairs`, 3단계 전 `e4c7f7e`와 후 `7daf406`, TPC-C W=1 64터미널 20초와 은행 질의, 여섯 쌍 번갈아,
+부하 평균 0.9–1.5).
+
+| | 전 | 후 |
+|---|---:|---:|
+| TPC-C 초당 거래 중앙값 | 42,900 | 43,220 (+0.7%) |
+| 거래당 CPU | 44.4 µs | 44.4 µs |
+| `per_branch`, `in_credit`, `large`, `ranks` | 5.31, 0.85, 0.38, 6.81 ms | +0.1%, −2.4%, 0.0%, −0.7% |
+
+쌍마다 TPC-C는 −3.8%에서 +6.2%로 흔들렸다(소유자의 코어 몫 0.87–0.91). 명령 수(025의 `count.py`, 전과 후를 두 번씩):
+
+| 한 번 물을 때 | 전 | 후 |
+|---|---:|---:|
+| `per_branch` | 26.980M | 26.827M (−0.57%) |
+| `in_credit` | 3.471M | 3.437M (−0.99%) |
+| `large` | 1.445M | 1.445M (+0.01%) |
+| `ranks` | 24.170M | 24.087M (−0.35%) |
+| `none`, `field`, `found` | 0.607M, 8.776M, 15.691M | +0.00%, +0.00%, +0.02% |
+
+요청이 해석한 모듈을 들고 다니고 소유자와 보기가 그것을 견주는 일(원자적 읽기와 포인터 비교)은 보이지 않는다.
+
+**남는 메모리**(`memory.py`). 항목 1,000개의 앱을 100번 바꿔 데운 뒤, 1,000번 더 바꾼 앞뒤의 자기만의 더티 메모리.
+
+| 바꾸는 것 | 앞 → 뒤 | 한 번에 |
+|---|---:|---:|
+| 모듈(두 파일을 번갈아, 바꿀 때마다 하나를 읽는다) | 3,120 → 21,952 KiB | 19,284 B |
+| 큐 예산만(모듈은 그대로) | 1,200 → 1,200 KiB | 0 B |
+
+한 번에 남는 것은 바뀐 모듈 하나(구조체 18,760 B와 모듈의 바이트)다.
+
+**검사**(`tests/check.py`의 `server_apps_lifecycle`). 한 호스트에서 앱을 더하고(바로 답한다), 지우고(404 `no_app`, 다른 프로세스가 그
+저장소를 연다), 바꾼다. 받는 앱 둘(같은 호스트의 앱이 안쪽 출구로, 다른 노드가 링크로 보내는)과 보내는 앱 둘을 보내기 한가운데에서
+바꾼다(바꿀 때 보낸 수가 0과 600 사이임을 단언한다). 받는 앱의 새 모듈은 입력의 번호가 다르고 보냄을 이름으로 받는다. 그동안
+목록 보기를 쉬지 않고 읽는다. 600건씩이 모두 한 번씩 순서대로 도착하고, 200을 받은 쓰기는 한 번씩 적용되고 503 `stopped`를 받은
+쓰기는 적용되지 않으며, 보기는 200이나 503 `stopped`만 답하고, 그대로인 앱은 하나도 실패하지 않는다. 저장소를 옮긴 앱은 새 저장소로
+다시 시작하고, 저장소를 만들 수 없던 앱은 고친 뒤의 SIGHUP에 시작하고, 틀린 줄이 있는 파일은 아무것도 바꾸지 않고, `kill -9` 뒤에는
+파일 그대로 시작한다. 혼자 10번, CPU 부하 아래 5번, ASan으로 빌드한 `pure-serve`로 6번 지났다.
+
+이 검사가 처음 구현에서 찾은 것: 보내는 앱이 멈추는 동안 받는 앱의 차례가 그 출구를 놓은 뒤에 읽는 자리(`local_done`), 앱이
+시작하기 전에 다른 앱의 출구가 그 소유자의 락을 쓰는 것(락을 자리와 함께 만든다).
+
+## 판정
+
+- **017의 3단계(수명)는 들어간다.** 네 조건을 모두 채웠다. 달리는 호스트의 앱은 SIGHUP과 파일로 더하고, 멈추고, 바꾼다.
+- 바꾸기는 그 자리에서 한다. 저장소가 바뀐 앱, 저장이 불확실해진 앱, 달리지 않던 앱만 저장소에서 다시 연다.
+- 파일을 읽는 스레드(`pure-apps`)가 여러 앱의 호스트마다 하나 늘었다. 앱 100개의 호스트는 스레드 넷이다(111에서 셋).
+
+## 다음
+
+- **바뀐 모듈을 놓기.** 지금은 바꿀 때마다 19 KB가 남는다(하루 한 번 바꾸면 한 해 7 MB). 놓으려면 그 모듈로 해석된 요청이
+ 끝났는지(본문을 받는 중인 요청은 `convert`에서 모듈을 다시 읽는다)와 연결 스레드의 리더가 다른 모듈로 옮겼는지를 알아야 한다.
+- **3b: 경로의 인증.** 앱의 경로를 누가 부를 수 있는지. 그 뒤에 HTTP로 앱을 더하고 바꾸는 길.
+- 기록의 모양이 바뀌는 큰 목록의 교체(100만 항목 172 ms)는 목록을 새 모양으로 다시 쓰는 값이다. 그동안 그 앱만 쉰다.
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl
new file mode 100644
index 0000000..ef2f8da
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 26963671, "cycles": 12117047}, "in_credit": {"instructions": 3467201, "cycles": 1640735}, "large": {"instructions": 1444754, "cycles": 661005}, "ranks": {"instructions": 24161321, "cycles": 15027140}, "none": {"instructions": 606875, "cycles": 190360}, "field": {"instructions": 8775999, "cycles": 3192408}, "found": {"instructions": 15689975, "cycles": 12571107}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26806316, "cycles": 13002802}, "in_credit": {"instructions": 3432116, "cycles": 1631717}, "large": {"instructions": 1444859, "cycles": 640909}, "ranks": {"instructions": 24076292, "cycles": 15205969}, "none": {"instructions": 606877, "cycles": 188197}, "field": {"instructions": 8776043, "cycles": 3139598}, "found": {"instructions": 15694123, "cycles": 12591284}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26996185, "cycles": 12941272}, "in_credit": {"instructions": 3474535, "cycles": 1773102}, "large": {"instructions": 1444802, "cycles": 647633}, "ranks": {"instructions": 24178845, "cycles": 15143526}, "none": {"instructions": 606875, "cycles": 189406}, "field": {"instructions": 8775999, "cycles": 3153492}, "found": {"instructions": 15692319, "cycles": 12541556}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26847256, "cycles": 11982057}, "in_credit": {"instructions": 3441216, "cycles": 1619426}, "large": {"instructions": 1444859, "cycles": 647753}, "ranks": {"instructions": 24096966, "cycles": 15147066}, "none": {"instructions": 606877, "cycles": 186693}, "field": {"instructions": 8776043, "cycles": 3133151}, "found": {"instructions": 15693340, "cycles": 12583251}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl
new file mode 100644
index 0000000..7cad56d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl
@@ -0,0 +1,11 @@
+{"round": 1, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10673, "completed": 106733, "2xx": 106733, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 92, "p99_us": 120, "max_us": 5888}
+{"round": 1, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 11075, "completed": 110753, "2xx": 110753, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 5376}
+{"round": 2, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 11225, "completed": 112254, "2xx": 112254, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 4608}
+{"round": 2, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10195, "completed": 101955, "2xx": 101955, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 120, "max_us": 4608}
+{"round": 3, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10829, "completed": 108295, "2xx": 108295, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 120, "max_us": 4608}
+{"round": 3, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 10323, "completed": 103233, "2xx": 103233, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 128, "max_us": 6400}
+{"round": 4, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 10461, "completed": 104614, "2xx": 104614, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 120, "max_us": 4352}
+{"round": 4, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10829, "completed": 108294, "2xx": 108294, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 120, "max_us": 4864}
+{"round": 5, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10061, "completed": 100607, "2xx": 100607, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 128, "max_us": 5632}
+{"round": 5, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 10761, "completed": 107611, "2xx": 107611, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 92, "p99_us": 116, "max_us": 3968}
+{"items": 1048576, "next": "items-next", "every_s": 0.2, "quiet_p99_us": 120, "changing_p99_us": 116, "ratio": 0.97}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl
new file mode 100644
index 0000000..f9783a7
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl
@@ -0,0 +1,11 @@
+{"round": 1, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10691, "completed": 106907, "2xx": 106907, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 92, "p99_us": 120, "max_us": 4352}
+{"round": 1, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11412, "completed": 114123, "2xx": 114123, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 124, "max_us": 5632}
+{"round": 2, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11523, "completed": 115235, "2xx": 115235, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 112, "max_us": 5888}
+{"round": 2, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10774, "completed": 107740, "2xx": 107740, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 88, "p99_us": 120, "max_us": 4608}
+{"round": 3, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10525, "completed": 105249, "2xx": 105249, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 120, "max_us": 4864}
+{"round": 3, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11493, "completed": 114934, "2xx": 114934, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 112, "max_us": 5888}
+{"round": 4, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11371, "completed": 113709, "2xx": 113709, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 116, "max_us": 4608}
+{"round": 4, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10925, "completed": 109252, "2xx": 109252, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 6144}
+{"round": 5, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 11110, "completed": 111099, "2xx": 111099, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 3968}
+{"round": 5, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11389, "completed": 113893, "2xx": 113893, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 120, "max_us": 5632}
+{"items": 1048576, "next": "items-wide", "every_s": 0.4, "quiet_p99_us": 120, "changing_p99_us": 116, "ratio": 0.97}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-load.txt b/research/experiments/030-app-lifecycle/2026-10-04-load.txt
new file mode 100644
index 0000000..5581ca3
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-load.txt
@@ -0,0 +1,12 @@
+1 before 10:45:44 up 7:07, 1 user, load average: 0.86, 0.74, 0.72
+1 in-place 10:47:11 up 7:08, 1 user, load average: 1.32, 0.98, 0.82
+2 in-place 10:48:37 up 7:10, 1 user, load average: 1.43, 1.17, 0.91
+2 before 10:50:04 up 7:11, 1 user, load average: 1.26, 1.20, 0.95
+3 before 10:51:50 up 7:13, 1 user, load average: 1.36, 1.36, 1.04
+3 in-place 10:53:47 up 7:15, 1 user, load average: 1.39, 1.50, 1.14
+4 in-place 10:55:20 up 7:16, 1 user, load average: 1.43, 1.54, 1.19
+4 before 10:56:47 up 7:18, 1 user, load average: 1.47, 1.60, 1.25
+5 before 10:58:24 up 7:19, 1 user, load average: 1.37, 1.59, 1.28
+5 in-place 10:59:50 up 7:21, 1 user, load average: 1.29, 1.55, 1.29
+6 in-place 11:01:17 up 7:22, 1 user, load average: 1.45, 1.58, 1.33
+6 before 11:02:43 up 7:24, 1 user, load average: 1.44, 1.60, 1.36
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl
new file mode 100644
index 0000000..ab87451
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl
@@ -0,0 +1,2 @@
+{"way": "module", "changes": 1000, "private_dirty_kib": [3120, 21952], "pss_kib": [3539, 22371], "per_change_bytes": 19284}
+{"way": "budget", "changes": 1000, "private_dirty_kib": [1200, 1200], "pss_kib": [1619, 1619], "per_change_bytes": 0}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl
new file mode 100644
index 0000000..b43320e
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.34, "pure_fastest_ms": 5.27, "sqlite_median_ms": 44.91, "sqlite_fastest_ms": 44.79, "sqlite_indexed_median_ms": 14.09, "sqlite_indexed_fastest_ms": 14.03}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.84, "pure_fastest_ms": 0.82, "sqlite_median_ms": 5.87, "sqlite_fastest_ms": 5.85, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.52, "pure_fastest_ms": 0.49, "sqlite_median_ms": 1.96, "sqlite_fastest_ms": 1.95, "sqlite_indexed_median_ms": 1.47, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 7.5, "pure_fastest_ms": 6.7, "sqlite_median_ms": 2833.76, "sqlite_fastest_ms": 2831.26, "sqlite_indexed_median_ms": 7.6, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl
new file mode 100644
index 0000000..bbc3032
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.32, "pure_fastest_ms": 5.22, "sqlite_median_ms": 45.34, "sqlite_fastest_ms": 45.13, "sqlite_indexed_median_ms": 14.06, "sqlite_indexed_fastest_ms": 14.03}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.84, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.88, "sqlite_fastest_ms": 5.87, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.73, "pure_fastest_ms": 6.69, "sqlite_median_ms": 2834.04, "sqlite_fastest_ms": 2830.76, "sqlite_indexed_median_ms": 7.62, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl
new file mode 100644
index 0000000..723657d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.26, "pure_fastest_ms": 5.19, "sqlite_median_ms": 44.89, "sqlite_fastest_ms": 44.8, "sqlite_indexed_median_ms": 13.98, "sqlite_indexed_fastest_ms": 13.96}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.9, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.87, "sqlite_fastest_ms": 5.85, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.78, "pure_fastest_ms": 6.66, "sqlite_median_ms": 2835.53, "sqlite_fastest_ms": 2831.69, "sqlite_indexed_median_ms": 7.55, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl
new file mode 100644
index 0000000..dda11e2
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.41, "pure_fastest_ms": 5.32, "sqlite_median_ms": 45.21, "sqlite_fastest_ms": 45.1, "sqlite_indexed_median_ms": 14.07, "sqlite_indexed_fastest_ms": 14.03}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.88, "pure_fastest_ms": 0.83, "sqlite_median_ms": 5.88, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.42, "pure_fastest_ms": 0.4, "sqlite_median_ms": 1.49, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.83, "pure_fastest_ms": 6.77, "sqlite_median_ms": 2835.74, "sqlite_fastest_ms": 2831.33, "sqlite_indexed_median_ms": 7.57, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl
new file mode 100644
index 0000000..8438db7
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.3, "pure_fastest_ms": 5.22, "sqlite_median_ms": 44.91, "sqlite_fastest_ms": 44.8, "sqlite_indexed_median_ms": 14.13, "sqlite_indexed_fastest_ms": 14.1}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.85, "pure_fastest_ms": 0.82, "sqlite_median_ms": 5.87, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.37, "sqlite_median_ms": 1.49, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.88, "pure_fastest_ms": 6.77, "sqlite_median_ms": 2839.51, "sqlite_fastest_ms": 2833.02, "sqlite_indexed_median_ms": 7.56, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl
new file mode 100644
index 0000000..6a6a729
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.1, "pure_fastest_ms": 5.05, "sqlite_median_ms": 44.93, "sqlite_fastest_ms": 44.77, "sqlite_indexed_median_ms": 14.1, "sqlite_indexed_fastest_ms": 14.01}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.85, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.9, "sqlite_fastest_ms": 5.88, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.74, "pure_fastest_ms": 6.67, "sqlite_median_ms": 2838.12, "sqlite_fastest_ms": 2835.12, "sqlite_indexed_median_ms": 7.58, "sqlite_indexed_fastest_ms": 7.52}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl
new file mode 100644
index 0000000..4cabca4
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.31, "pure_fastest_ms": 5.25, "sqlite_median_ms": 45.44, "sqlite_fastest_ms": 45.18, "sqlite_indexed_median_ms": 14.09, "sqlite_indexed_fastest_ms": 14.01}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.91, "pure_fastest_ms": 0.83, "sqlite_median_ms": 5.95, "sqlite_fastest_ms": 5.89, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.39, "pure_fastest_ms": 0.37, "sqlite_median_ms": 1.47, "sqlite_fastest_ms": 1.46, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.46}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.85, "pure_fastest_ms": 6.76, "sqlite_median_ms": 2833.97, "sqlite_fastest_ms": 2829.69, "sqlite_indexed_median_ms": 7.55, "sqlite_indexed_fastest_ms": 7.52}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl
new file mode 100644
index 0000000..c4fb377
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.32, "pure_fastest_ms": 5.24, "sqlite_median_ms": 44.87, "sqlite_fastest_ms": 44.8, "sqlite_indexed_median_ms": 14.08, "sqlite_indexed_fastest_ms": 14.05}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.82, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.89, "sqlite_fastest_ms": 5.87, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.47, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.75, "pure_fastest_ms": 6.7, "sqlite_median_ms": 2835.9, "sqlite_fastest_ms": 2833.26, "sqlite_indexed_median_ms": 7.55, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl
new file mode 100644
index 0000000..054658b
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.38, "pure_fastest_ms": 5.26, "sqlite_median_ms": 45.3, "sqlite_fastest_ms": 45.05, "sqlite_indexed_median_ms": 13.99, "sqlite_indexed_fastest_ms": 13.93}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.83, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.9, "sqlite_fastest_ms": 5.87, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.37, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.47, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.47, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.76, "pure_fastest_ms": 6.74, "sqlite_median_ms": 2836.89, "sqlite_fastest_ms": 2832.35, "sqlite_indexed_median_ms": 7.58, "sqlite_indexed_fastest_ms": 7.49}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl
new file mode 100644
index 0000000..1a53621
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.3, "pure_fastest_ms": 5.23, "sqlite_median_ms": 45.78, "sqlite_fastest_ms": 45.2, "sqlite_indexed_median_ms": 14.05, "sqlite_indexed_fastest_ms": 14.01}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 1.0, "pure_fastest_ms": 0.85, "sqlite_median_ms": 5.9, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.36, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.8, "pure_fastest_ms": 6.72, "sqlite_median_ms": 2838.86, "sqlite_fastest_ms": 2835.33, "sqlite_indexed_median_ms": 7.59, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl
new file mode 100644
index 0000000..507eac9
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.32, "pure_fastest_ms": 5.24, "sqlite_median_ms": 45.19, "sqlite_fastest_ms": 45.04, "sqlite_indexed_median_ms": 14.05, "sqlite_indexed_fastest_ms": 14.02}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.83, "pure_fastest_ms": 0.8, "sqlite_median_ms": 5.89, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.71, "pure_fastest_ms": 6.64, "sqlite_median_ms": 2834.72, "sqlite_fastest_ms": 2831.67, "sqlite_indexed_median_ms": 7.71, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl
new file mode 100644
index 0000000..dd21361
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.23, "pure_fastest_ms": 5.13, "sqlite_median_ms": 45.19, "sqlite_fastest_ms": 45.06, "sqlite_indexed_median_ms": 14.12, "sqlite_indexed_fastest_ms": 14.07}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.83, "pure_fastest_ms": 0.79, "sqlite_median_ms": 5.88, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.4, "pure_fastest_ms": 0.37, "sqlite_median_ms": 1.5, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.7, "pure_fastest_ms": 6.63, "sqlite_median_ms": 2835.61, "sqlite_fastest_ms": 2833.18, "sqlite_indexed_median_ms": 7.56, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl
new file mode 100644
index 0000000..ac6ece1
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.61, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 41908, "new_orders_per_minute": 1121396, "undone_new_orders": 3571, "errors": 0, "new_order": {"done": 377370, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "payment": {"done": 360166, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "order_status": {"done": 33677, "failed": 0, "p50_us": 256, "p99_us": 1472}, "delivery": {"done": 33572, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "stock_level": {"done": 33380, "failed": 0, "p50_us": 288, "p99_us": 1600}, "server_threads": "3", "cpu_us_per_transaction": 44.2, "core_shares": {"pure-owner": 0.88, "pure-checkpoint": 0.01}, "resident_mib": 441.5, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 441.5, "pure_state_mib": 224.1, "pure_store_mib": 230.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25495, "burst_events": 767622, "checkpoints": 37}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl
new file mode 100644
index 0000000..53ff411
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.57, "pure_load_s": 0.72, "sqlite_load_s": 1.57, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 40884, "new_orders_per_minute": 1093955, "undone_new_orders": 3508, "errors": 0, "new_order": {"done": 368160, "failed": 0, "p50_us": 1536, "p99_us": 4352}, "payment": {"done": 351436, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "order_status": {"done": 32835, "failed": 0, "p50_us": 272, "p99_us": 1472}, "delivery": {"done": 32724, "failed": 0, "p50_us": 1472, "p99_us": 4096}, "stock_level": {"done": 32531, "failed": 0, "p50_us": 304, "p99_us": 1600}, "server_threads": "3", "cpu_us_per_transaction": 45.1, "core_shares": {"pure-owner": 0.87, "pure-checkpoint": 0.01}, "resident_mib": 432.0, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 432.0, "pure_state_mib": 219.2, "pure_store_mib": 222.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25009, "burst_events": 748898, "checkpoints": 37}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl
new file mode 100644
index 0000000..cf33a75
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.58, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42900, "new_orders_per_minute": 1147828, "undone_new_orders": 3658, "errors": 0, "new_order": {"done": 386268, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "payment": {"done": 368740, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "order_status": {"done": 34439, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 34372, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "stock_level": {"done": 34175, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 44.0, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 449.7, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 449.7, "pure_state_mib": 228.9, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26093, "burst_events": 785803, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl
new file mode 100644
index 0000000..4bf8c0c
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.73, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42901, "new_orders_per_minute": 1147777, "undone_new_orders": 3672, "errors": 0, "new_order": {"done": 386265, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "payment": {"done": 368757, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "order_status": {"done": 34442, "failed": 0, "p50_us": 256, "p99_us": 1280}, "delivery": {"done": 34374, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "stock_level": {"done": 34187, "failed": 0, "p50_us": 304, "p99_us": 1408}, "server_threads": "3", "cpu_us_per_transaction": 44.7, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 450.2, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 450.2, "pure_state_mib": 228.9, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26389, "burst_events": 785808, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl
new file mode 100644
index 0000000..131a025
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.74, "sqlite_load_s": 1.6, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42976, "new_orders_per_minute": 1149663, "undone_new_orders": 3673, "errors": 0, "new_order": {"done": 386895, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "payment": {"done": 369465, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "order_status": {"done": 34484, "failed": 0, "p50_us": 256, "p99_us": 1280}, "delivery": {"done": 34429, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "stock_level": {"done": 34244, "failed": 0, "p50_us": 288, "p99_us": 1408}, "server_threads": "3", "cpu_us_per_transaction": 44.6, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 451.8, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 451.8, "pure_state_mib": 229.2, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26247, "burst_events": 787183, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl
new file mode 100644
index 0000000..98d082d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.6, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43358, "new_orders_per_minute": 1159818, "undone_new_orders": 3705, "errors": 0, "new_order": {"done": 390316, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "payment": {"done": 372786, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "order_status": {"done": 34772, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 34732, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "stock_level": {"done": 34568, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 44.0, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 454.9, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 454.9, "pure_state_mib": 231.1, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26292, "burst_events": 794213, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl
new file mode 100644
index 0000000..3d0a8ff
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.56, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43908, "new_orders_per_minute": 1174706, "undone_new_orders": 3766, "errors": 0, "new_order": {"done": 395335, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "payment": {"done": 377415, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "order_status": {"done": 35217, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 35180, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "stock_level": {"done": 35012, "failed": 0, "p50_us": 288, "p99_us": 1216}, "server_threads": "3", "cpu_us_per_transaction": 44.4, "core_shares": {"pure-owner": 0.91, "pure-checkpoint": 0.01}, "resident_mib": 459.9, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 459.9, "pure_state_mib": 233.7, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26826, "burst_events": 804248, "checkpoints": 39}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl
new file mode 100644
index 0000000..cecc96a
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.58, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43426, "new_orders_per_minute": 1161961, "undone_new_orders": 3714, "errors": 0, "new_order": {"done": 391037, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "payment": {"done": 373282, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "order_status": {"done": 34830, "failed": 0, "p50_us": 256, "p99_us": 1280}, "delivery": {"done": 34765, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "stock_level": {"done": 34611, "failed": 0, "p50_us": 304, "p99_us": 1344}, "server_threads": "3", "cpu_us_per_transaction": 44.1, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 455.0, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 455.0, "pure_state_mib": 231.5, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26610, "burst_events": 795456, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl
new file mode 100644
index 0000000..97318be
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.64, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42899, "new_orders_per_minute": 1147829, "undone_new_orders": 3662, "errors": 0, "new_order": {"done": 386274, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "payment": {"done": 368750, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "order_status": {"done": 34423, "failed": 0, "p50_us": 256, "p99_us": 1216}, "delivery": {"done": 34332, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "stock_level": {"done": 34209, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 44.5, "core_shares": {"pure-owner": 0.89, "pure-checkpoint": 0.01}, "resident_mib": 450.4, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 450.4, "pure_state_mib": 228.9, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25746, "burst_events": 785777, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl
new file mode 100644
index 0000000..e7978fd
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.58, "pure_load_s": 0.73, "sqlite_load_s": 1.6, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 41272, "new_orders_per_minute": 1104070, "undone_new_orders": 3527, "errors": 0, "new_order": {"done": 371551, "failed": 0, "p50_us": 1472, "p99_us": 4608}, "payment": {"done": 354807, "failed": 0, "p50_us": 1472, "p99_us": 4608}, "order_status": {"done": 33181, "failed": 0, "p50_us": 272, "p99_us": 1600}, "delivery": {"done": 33061, "failed": 0, "p50_us": 1472, "p99_us": 4608}, "stock_level": {"done": 32844, "failed": 0, "p50_us": 304, "p99_us": 1856}, "server_threads": "3", "cpu_us_per_transaction": 45.1, "core_shares": {"pure-owner": 0.87, "pure-checkpoint": 0.01}, "resident_mib": 436.2, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 436.2, "pure_state_mib": 221.0, "pure_store_mib": 230.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25301, "burst_events": 755969, "checkpoints": 37}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl
new file mode 100644
index 0000000..1c2444d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.58, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43015, "new_orders_per_minute": 1150991, "undone_new_orders": 3677, "errors": 0, "new_order": {"done": 387341, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "payment": {"done": 369725, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "order_status": {"done": 34509, "failed": 0, "p50_us": 256, "p99_us": 1344}, "delivery": {"done": 34443, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "stock_level": {"done": 34277, "failed": 0, "p50_us": 304, "p99_us": 1408}, "server_threads": "3", "cpu_us_per_transaction": 44.4, "core_shares": {"pure-owner": 0.89, "pure-checkpoint": 0.01}, "resident_mib": 451.2, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 451.2, "pure_state_mib": 229.5, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26384, "burst_events": 787918, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl
new file mode 100644
index 0000000..8980002
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.57, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 44046, "new_orders_per_minute": 1178462, "undone_new_orders": 3781, "errors": 0, "new_order": {"done": 396614, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "payment": {"done": 378619, "failed": 0, "p50_us": 1408, "p99_us": 3328}, "order_status": {"done": 35311, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 35287, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "stock_level": {"done": 35114, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 43.8, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 460.6, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 460.6, "pure_state_mib": 234.4, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26765, "burst_events": 806824, "checkpoints": 39}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl
new file mode 100644
index 0000000..fa51827
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.44, "reading_ms": 0.16}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 2.92, "reading_ms": 0.23}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.54, "reading_ms": 0.26}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.38, "reading_ms": 0.24}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.15, "reading_ms": 1.24}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.09, "reading_ms": 1.2}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.16, "reading_ms": 1.25}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.09, "reading_ms": 1.2}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.53, "reading_ms": 0.23}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.65, "reading_ms": 0.26}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 3.19, "reading_ms": 0.28}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.39, "reading_ms": 0.25}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.06, "reading_ms": 0.26}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 3.2, "reading_ms": 1.24}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.47, "reading_ms": 0.28}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.35, "reading_ms": 0.22}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.05, "reading_ms": 1.21}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.53, "reading_ms": 0.21}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 2.15, "reading_ms": 0.2}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 2.92, "reading_ms": 0.16}
+{"items": 1024, "way": "restarted", "gap_ms": 2.77}
+{"items": 1024, "way": "restarted", "gap_ms": 2.32}
+{"items": 1024, "way": "restarted", "gap_ms": 2.7}
+{"items": 1024, "way": "restarted", "gap_ms": 2.32}
+{"items": 1024, "way": "restarted", "gap_ms": 2.53}
+{"items": 1024, "way": "restarted", "gap_ms": 2.35}
+{"items": 1024, "way": "restarted", "gap_ms": 2.43}
+{"items": 1024, "way": "restarted", "gap_ms": 2.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.72}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "changed_median_ms": 1.06, "restarted_median_ms": 2.42, "refused": 63, "answered": 66300}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl
new file mode 100644
index 0000000..4fbda76
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 2.76, "reading_ms": 0.39}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 3.08, "reading_ms": 4.92}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.61, "reading_ms": 0.37}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.34, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.37, "reading_ms": 1.45}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.29, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.32, "reading_ms": 1.43}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.27, "reading_ms": 1.4}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.44, "reading_ms": 0.38}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.35, "reading_ms": 1.43}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.74, "reading_ms": 0.34}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.34, "reading_ms": 0.34}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.49, "reading_ms": 0.44}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 2.15, "reading_ms": 0.37}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.74, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.6, "reading_ms": 0.32}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.94, "reading_ms": 0.43}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.4, "reading_ms": 0.47}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.3, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.3, "reading_ms": 1.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.88}
+{"items": 1024, "way": "restarted", "gap_ms": 2.7}
+{"items": 1024, "way": "restarted", "gap_ms": 2.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.43}
+{"items": 1024, "way": "restarted", "gap_ms": 2.8}
+{"items": 1024, "way": "restarted", "gap_ms": 2.54}
+{"items": 1024, "way": "restarted", "gap_ms": 2.52}
+{"items": 1024, "way": "restarted", "gap_ms": 2.49}
+{"items": 1024, "way": "restarted", "gap_ms": 2.47}
+{"items": 1024, "way": "restarted", "gap_ms": 2.43}
+{"items": 1024, "changed_median_ms": 1.31, "restarted_median_ms": 2.51, "refused": 130, "answered": 66485}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl
new file mode 100644
index 0000000..7ceccc0
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.41, "reading_ms": 0.3}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 3.91, "reading_ms": 1.37}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.54, "reading_ms": 0.25}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.43, "reading_ms": 0.22}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.1, "reading_ms": 1.24}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.19, "reading_ms": 1.28}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.14, "reading_ms": 1.21}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 4.13, "reading_ms": 0.21}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.92, "reading_ms": 0.22}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.3, "reading_ms": 0.22}
+{"items": 1048576, "way": "restarted", "gap_ms": 151.32}
+{"items": 1048576, "way": "restarted", "gap_ms": 150.63}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.28}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.94}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.65}
+{"items": 1048576, "changed_median_ms": 1.12, "restarted_median_ms": 149.94, "refused": 45, "answered": 34645}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl
new file mode 100644
index 0000000..3b08f1c
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 178.42, "reading_ms": 175.85}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 160.31, "reading_ms": 157.02}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 177.96, "reading_ms": 175.48}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 161.7, "reading_ms": 158.05}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 181.63, "reading_ms": 178.71}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 163.76, "reading_ms": 159.67}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 182.44, "reading_ms": 178.78}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 163.71, "reading_ms": 159.36}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 185.21, "reading_ms": 180.69}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 165.13, "reading_ms": 160.48}
+{"items": 1048576, "way": "restarted", "gap_ms": 332.42}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.08}
+{"items": 1048576, "way": "restarted", "gap_ms": 327.97}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.43}
+{"items": 1048576, "way": "restarted", "gap_ms": 328.56}
+{"items": 1048576, "changed_median_ms": 171.54, "restarted_median_ms": 332.42, "refused": 20534, "answered": 34071}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl
new file mode 100644
index 0000000..15c59fb
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.5, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.53, "reading_ms": 1.78}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 0.55, "reading_ms": 0.57}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.57, "reading_ms": 1.63}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.51, "reading_ms": 1.6}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.7, "reading_ms": 0.68}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 0.78, "reading_ms": 0.54}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 3.99, "reading_ms": 0.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.48, "reading_ms": 1.61}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.6, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.54, "reading_ms": 1.63}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.67}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.49, "reading_ms": 1.63}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.3, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.51, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.54, "reading_ms": 1.69}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.56, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 3.08, "reading_ms": 0.71}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.49, "reading_ms": 1.64}
+{"items": 1024, "way": "restarted", "gap_ms": 2.49}
+{"items": 1024, "way": "restarted", "gap_ms": 2.35}
+{"items": 1024, "way": "restarted", "gap_ms": 2.45}
+{"items": 1024, "way": "restarted", "gap_ms": 2.42}
+{"items": 1024, "way": "restarted", "gap_ms": 3.89}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "way": "restarted", "gap_ms": 2.42}
+{"items": 1024, "way": "restarted", "gap_ms": 2.54}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "way": "restarted", "gap_ms": 2.42}
+{"items": 1024, "changed_median_ms": 1.52, "restarted_median_ms": 2.42, "refused": 253, "answered": 64398}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl
new file mode 100644
index 0000000..81f153d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 3.4, "reading_ms": 0.77}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.5, "reading_ms": 0.86}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.57, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 4.02, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 5.3, "reading_ms": 1.79}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.66, "reading_ms": 0.66}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 0.59, "reading_ms": 0.63}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 4.01, "reading_ms": 0.6}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 0.52, "reading_ms": 0.61}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.52, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 2.04, "reading_ms": 1.68}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.58, "reading_ms": 1.68}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 2.23, "reading_ms": 0.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 2.37, "reading_ms": 0.58}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.69}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.53, "reading_ms": 0.59}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.31, "reading_ms": 0.58}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.94, "reading_ms": 0.6}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.57, "reading_ms": 1.66}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.65}
+{"items": 1024, "way": "restarted", "gap_ms": 2.73}
+{"items": 1024, "way": "restarted", "gap_ms": 2.44}
+{"items": 1024, "way": "restarted", "gap_ms": 2.36}
+{"items": 1024, "way": "restarted", "gap_ms": 3.84}
+{"items": 1024, "way": "restarted", "gap_ms": 2.66}
+{"items": 1024, "way": "restarted", "gap_ms": 2.8}
+{"items": 1024, "way": "restarted", "gap_ms": 2.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "way": "restarted", "gap_ms": 2.63}
+{"items": 1024, "way": "restarted", "gap_ms": 4.49}
+{"items": 1024, "changed_median_ms": 1.58, "restarted_median_ms": 2.64, "refused": 173, "answered": 62631}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl
new file mode 100644
index 0000000..23d81fd
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 151.25, "reading_ms": 151.2}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 149.93, "reading_ms": 150.0}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 150.36, "reading_ms": 150.34}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 151.27, "reading_ms": 151.31}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 149.72, "reading_ms": 149.74}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 150.48, "reading_ms": 150.5}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 149.59, "reading_ms": 149.58}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 150.75, "reading_ms": 150.88}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 149.79, "reading_ms": 149.8}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 153.03, "reading_ms": 153.06}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.63}
+{"items": 1048576, "way": "restarted", "gap_ms": 148.91}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.63}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.21}
+{"items": 1048576, "way": "restarted", "gap_ms": 151.98}
+{"items": 1048576, "changed_median_ms": 150.42, "restarted_median_ms": 149.63, "refused": 18133, "answered": 33199}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl
new file mode 100644
index 0000000..07a3b26
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 329.24, "reading_ms": 329.21}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 339.41, "reading_ms": 339.48}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 335.25, "reading_ms": 335.32}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 345.5, "reading_ms": 345.51}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 328.75, "reading_ms": 328.83}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 342.24, "reading_ms": 342.22}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 328.17, "reading_ms": 328.22}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 343.68, "reading_ms": 343.74}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 332.61, "reading_ms": 332.6}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 340.55, "reading_ms": 339.73}
+{"items": 1048576, "way": "restarted", "gap_ms": 329.85}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.79}
+{"items": 1048576, "way": "restarted", "gap_ms": 327.33}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.1}
+{"items": 1048576, "way": "restarted", "gap_ms": 329.47}
+{"items": 1048576, "changed_median_ms": 337.33, "restarted_median_ms": 329.85, "refused": 40844, "answered": 33921}
diff --git a/research/experiments/030-app-lifecycle/isolation.py b/research/experiments/030-app-lifecycle/isolation.py
new file mode 100644
index 0000000..43259f4
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/isolation.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""Another app's latency while one app changes again and again on a running
+host (docs/design/017's stage 3, research 115).
+
+A host of apps (two connection threads, two workers, stores in RAM, cores
+0-2) holds A, an app of items.aup with ITEMS items, and B, the scoreboard
+(benchmarks/http/scores.aup). B takes one connection's scores
+(benchmarks/http/load on core 3) for SECONDS, while A is left be (quiet)
+or changes every EVERY seconds (changing: SIGHUP, its line naming in turn
+items.pmod and NEXT). The two take turns, their order turned each round.
+
+isolation.py PURE_SERVE ITEMS NEXT EVERY SECONDS ROUNDS a JSON line per run,
+ then one with medians
+"""
+import json
+import select
+import shutil
+import signal
+import statistics
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from pathlib import Path
+
+from window import AUP, HERE, grow
+
+ROOT = HERE.parents[2]
+LOAD = ROOT / 'benchmarks' / 'http' / 'load'
+
+
+def main():
+ serve, items, after = Path(sys.argv[1]).resolve(), int(sys.argv[2]), sys.argv[3]
+ every, seconds, rounds = float(sys.argv[4]), int(sys.argv[5]), int(sys.argv[6])
+ work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm'))
+ try:
+ modules = {}
+ for name, source in (('items', HERE / 'items.aup'), (after, HERE / f'{after}.aup'),
+ ('scores', ROOT / 'benchmarks' / 'http' / 'scores.aup')):
+ modules[name] = work / f'{name}.pmod'
+ subprocess.run([str(AUP), 'build', str(source), '-o', str(modules[name])], check=True)
+ conf = work / 'apps.conf'
+
+ def write(module):
+ conf.write_text(f"a {modules[module]} {work / 'a'}\nb {modules['scores']} {work / 'b'}\n")
+
+ write('items')
+ host = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--apps', str(conf), '--port', '0', '--threads', '2',
+ '--workers', '2'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ port = json.loads(host.stdout.readline())['port']
+ count = grow(port, items, 'a')
+ p99 = {'quiet': [], 'changing': []}
+ changes = 0
+ for round_ in range(1, rounds + 1):
+ for way in (['quiet', 'changing'] if round_ % 2 else ['changing', 'quiet']):
+ load = subprocess.Popen(['taskset', '-c', '3', str(LOAD), str(port), '1', str(seconds), 'score', '1',
+ 'keep-alive', '16', '/b'], stdout=subprocess.PIPE, text=True)
+ made = 0
+ while way == 'changing' and load.poll() is None:
+ write(after if changes % 2 == 0 else 'items')
+ host.send_signal(signal.SIGHUP)
+ ready, _, _ = select.select([host.stdout], [], [], 60)
+ assert ready
+ said = json.loads(host.stdout.readline())
+ assert said['apps'][0]['running'], said
+ changes += 1
+ made += 1
+ time.sleep(every)
+ out, _ = load.communicate()
+ result = json.loads(out)
+ assert result['5xx'] == 0 and result['errors'] == 0, result
+ p99[way].append(result['p99_us'])
+ print(json.dumps({'round': round_, 'way': way, 'items': count, 'next': after, 'changes': made, **result}),
+ flush=True)
+ host.terminate()
+ host.communicate(timeout=600)
+ quiet, changing = statistics.median(p99['quiet']), statistics.median(p99['changing'])
+ print(json.dumps({'items': count, 'next': after, 'every_s': every, 'quiet_p99_us': quiet,
+ 'changing_p99_us': changing, 'ratio': round(changing / quiet, 2)}), flush=True)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/research/experiments/030-app-lifecycle/items-wide.aup b/research/experiments/030-app-lifecycle/items-wide.aup
new file mode 100644
index 0000000..6b28477
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/items-wide.aup
@@ -0,0 +1,29 @@
+record Item {
+ n = 0
+ m = 0
+}
+state items: list of Item
+state more: list of Item
+state hits = 0
+input seed = message { n: number }
+input grow = message { step: number }
+input add = message { step: number }
+on seed {
+ items.append(Item { n: seed.n })
+}
+on grow {
+ for i in items {
+ more.append(Item { n: i.n + grow.step })
+ }
+ for m in more {
+ items.append(Item { n: m.n })
+ }
+ for m in more {
+ more.remove(m)
+ }
+}
+on add {
+ hits = hits + add.step
+}
+view count = count(i in items where i.n > 0)
+view total = hits
diff --git a/research/experiments/030-app-lifecycle/memory.py b/research/experiments/030-app-lifecycle/memory.py
new file mode 100644
index 0000000..3c15d4a
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/memory.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""What changing an app leaves in a running host's memory (docs/design/017's
+stage 3, research 115). A host of apps holds an app of items.aup (a
+thousand items, its store in RAM); its own memory (smaps_rollup's
+Private_Dirty and Pss) is read after a warm-up of changes, then after
+CHANGES changes more: in place to another module (its line naming in turn
+items.pmod and items-next.pmod: each reading loads a module, and keeps the
+one it replaced), and with its module as it was (its queue budget changed
+in turn: no module loaded). The difference a change.
+
+memory.py PURE_SERVE CHANGES a JSON line per way
+"""
+import json
+import select
+import shutil
+import signal
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+from window import AUP, HERE, grow
+
+
+def own(pid):
+ fields = {}
+ for line in Path(f'/proc/{pid}/smaps_rollup').read_text().splitlines()[1:]:
+ name, value = line.split(':', 1)
+ fields[name] = int(value.split()[0])
+ return fields['Private_Dirty'], fields['Pss']
+
+
+def main():
+ serve, changes = Path(sys.argv[1]).resolve(), int(sys.argv[2])
+ work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm'))
+ try:
+ modules = {}
+ for name in ('items', 'items-next'):
+ modules[name] = work / f'{name}.pmod'
+ subprocess.run([str(AUP), 'build', str(HERE / f'{name}.aup'), '-o', str(modules[name])], check=True)
+ for way in ('module', 'budget'):
+ conf = work / 'apps.conf'
+
+ def write(n):
+ module = 'items-next' if way == 'module' and n % 2 else 'items'
+ budget = f' queue={1000 + n % 2}' if way == 'budget' else ''
+ conf.write_text(f"x {modules[module]} {work / way}{budget}\n")
+
+ write(0)
+ host = subprocess.Popen([str(serve), '--apps', str(conf), '--port', '0'], stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ port = json.loads(host.stdout.readline())['port']
+ grow(port, 1000)
+
+ def change(n):
+ write(n)
+ host.send_signal(signal.SIGHUP)
+ ready, _, _ = select.select([host.stdout], [], [], 60)
+ assert ready
+ said = json.loads(host.stdout.readline())
+ assert said['apps'][0]['did'] == 'changed' and said['apps'][0]['running'], said
+
+ for n in range(1, 101): # warm: the allocator's arenas settle
+ change(n)
+ dirty, pss = own(host.pid)
+ for n in range(101, 101 + changes):
+ change(n)
+ dirty_after, pss_after = own(host.pid)
+ host.terminate()
+ host.communicate(timeout=60)
+ print(json.dumps({'way': way, 'changes': changes, 'private_dirty_kib': [dirty, dirty_after],
+ 'pss_kib': [pss, pss_after],
+ 'per_change_bytes': round((dirty_after - dirty) * 1024 / changes)}), flush=True)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/research/experiments/030-app-lifecycle/pairs.py b/research/experiments/030-app-lifecycle/pairs.py
new file mode 100644
index 0000000..2ea4f48
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/pairs.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""The pairs of runs.sh pairs as a table (research 115): each round's TPC-C
+(transactions a second, CPU a transaction, the owner's core share) before
+and after, the medians and the bank queries' median times.
+
+pairs.py DATE from DATE-tpcc-*.jsonl and DATE-queries-*.jsonl beside it
+"""
+import json
+import statistics
+import sys
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+day = sys.argv[1]
+
+
+def tpcc(version, round_):
+ for line in (HERE / f'{day}-tpcc-{version}-{round_}.jsonl').read_text().splitlines():
+ r = json.loads(line)
+ if r.get('step') == 'run' and r.get('system') == 'pure':
+ owner = max((share for name, share in r['core_shares'].items() if 'owner' in name), default=None) \
+ if isinstance(r['core_shares'], dict) else None
+ return r['transactions_per_second'], r['cpu_us_per_transaction'], owner
+
+
+def queries(version, round_):
+ return {r['query']: r['pure_median_ms'] for r in map(json.loads, (HERE / f'{day}-queries-{version}-{round_}.jsonl')
+ .read_text().splitlines()) if 'query' in r}
+
+
+rounds = sorted({int(p.stem.rsplit('-', 1)[1]) for p in HERE.glob(f'{day}-tpcc-before-*.jsonl')})
+rows = {'before': [], 'in-place': []}
+for round_ in rounds:
+ b, a = tpcc('before', round_), tpcc('in-place', round_)
+ rows['before'].append(b), rows['in-place'].append(a)
+ print(json.dumps({'round': round_, 'tps': [b[0], a[0]], 'change': round((a[0] / b[0] - 1) * 100, 1),
+ 'cpu_us': [b[1], a[1]], 'owner_share': [b[2], a[2]]}))
+med = {v: statistics.median(t for t, _, _ in rows[v]) for v in rows}
+cpu = {v: statistics.median(c for _, c, _ in rows[v]) for v in rows}
+names = queries('before', rounds[0]).keys()
+q = {v: {n: statistics.median(queries(v, r)[n] for r in rounds) for n in names} for v in rows}
+print(json.dumps({'tps_median': [med['before'], med['in-place']], 'change': round((med['in-place'] / med['before'] - 1) * 100, 1),
+ 'cpu_us_median': [cpu['before'], cpu['in-place']],
+ 'queries_median_ms': {n: [q['before'][n], q['in-place'][n],
+ round((q['in-place'][n] / q['before'][n] - 1) * 100, 1)] for n in names}}))
diff --git a/research/experiments/030-app-lifecycle/runs.sh b/research/experiments/030-app-lifecycle/runs.sh
index 601f754..0a70921 100755
--- a/research/experiments/030-app-lifecycle/runs.sh
+++ b/research/experiments/030-app-lifecycle/runs.sh
@@ -5,6 +5,15 @@
# in ~/.cache/pure-030/NAME
# runs.sh window NAME ITEMS CHANGES RESTARTS [MODULE] window.py on that
# build, into DATE-window-NAME-ITEMS[-MODULE].jsonl
+# runs.sh isolation NAME ITEMS NEXT EVERY SECONDS ROUNDS isolation.py,
+# into DATE-isolation-NAME-ITEMS-NEXT.jsonl
+# runs.sh pairs FIRST LAST a host of one app, the builds before (e4c7f7e,
+# stage 3's criteria) and after in turns, as
+# 026 ran them: TPC-C in RAM (W=1, 64
+# terminals, 20 s) and the bank's queries
+# runs.sh counts the bank queries' instructions before and
+# after (025's count.py)
+# runs.sh memory CHANGES what changing an app leaves in memory (memory.py)
set -e
out=research/experiments/030-app-lifecycle
builds=$HOME/.cache/pure-030
@@ -21,5 +30,29 @@ window)
until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
python3 research/guard.py --most 2048 -- python3 $out/window.py "$builds/$2/pure-serve" "$3" "$4" "$5" ${6:+"$6"} \
> "$out/$day-window-$2-$3${6:+-$6}.jsonl" ;;
-*) echo "runs.sh build NAME COMMIT | window NAME ITEMS CHANGES RESTARTS [MODULE]" >&2; exit 2 ;;
+pairs)
+ for round in $(seq "$2" "$3"); do
+ first=before second=in-place
+ if [ $((round % 2)) = 0 ]; then first=in-place second=before; fi
+ for version in $first $second; do
+ until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
+ echo "$round $version $(uptime)" >> "$out/$day-load.txt"
+ python3 research/guard.py -- python3 benchmarks/tpcc/run.py --serve "$builds/$version/pure-serve" --warehouses 1 \
+ --check 0 --terminals 64 --sqlite-threads '' --sqlite-group '' --seconds 20 \
+ > "$out/$day-tpcc-$version-$round.jsonl"
+ python3 research/guard.py -- python3 benchmarks/bank/queries.py --serve "$builds/$version/pure-serve" \
+ > "$out/$day-queries-$version-$round.jsonl"
+ done
+ done ;;
+counts)
+ python3 research/guard.py -- python3 research/experiments/025-i64-cost/count.py "$builds/before/pure-serve" \
+ "$builds/in-place/pure-serve" "$builds/before/pure-serve" "$builds/in-place/pure-serve" > "$out/$day-counts.jsonl" ;;
+memory)
+ python3 research/guard.py -- python3 $out/memory.py "$builds/in-place/pure-serve" "$2" > "$out/$day-memory.jsonl" ;;
+isolation)
+ until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
+ python3 research/guard.py --most 2048 -- python3 $out/isolation.py "$builds/$2/pure-serve" "$3" "$4" "$5" "$6" "$7" \
+ > "$out/$day-isolation-$2-$3-$4.jsonl" ;;
+*) echo "runs.sh build NAME COMMIT | window NAME ITEMS CHANGES RESTARTS [MODULE] | isolation NAME ITEMS NEXT EVERY SECONDS ROUNDS | pairs FIRST LAST | counts | memory CHANGES" >&2
+ exit 2 ;;
esac
diff --git a/research/experiments/030-app-lifecycle/window.py b/research/experiments/030-app-lifecycle/window.py
index 3ec21a5..3c1d511 100755
--- a/research/experiments/030-app-lifecycle/window.py
+++ b/research/experiments/030-app-lifecycle/window.py
@@ -7,12 +7,16 @@ of apps, its store in RAM, the host on cores 0-2. A client on core 3 posts
writes to it one after another on one connection; the time an app cannot
answer is the longest gap between two answered writes (200) around the
change. Changed: SIGHUP after its line names the other of two module files
-(items.pmod, items-next.pmod: the same states, a view more), so each
-reading starts it again on another module. Restarted: SIGTERM, then the
-host started again (the client reconnecting at once).
-
-window.py PURE_SERVE ITEMS CHANGES RESTARTS a JSON line per change and per
- restart, then one with medians
+(items.pmod and NEXT, items-next.pmod unless named: the same states, a
+view more; items-wide.pmod: its records a field more, so the list is laid
+out anew), so each reading changes it to another module. Restarted: the
+same change made the way there was before, the line naming the other
+module, then SIGTERM and the host started again (the client reconnecting
+at once).
+
+window.py PURE_SERVE ITEMS CHANGES RESTARTS [NEXT] a JSON line per change
+ and per restart, then one with
+ medians
"""
import http.client
import json
@@ -93,7 +97,7 @@ def free_port():
return probe.getsockname()[1]
-def grow(port, items):
+def grow(port, items, app='x'):
connection = http.client.HTTPConnection('127.0.0.1', port, timeout=600)
def post(path):
@@ -102,10 +106,10 @@ def grow(port, items):
answer.read()
assert answer.status == 200, (path, answer.status)
- post('/x/inputs/seed?n=1')
+ post(f'/{app}/inputs/seed?n=1')
count = 1
while count < items:
- post('/x/inputs/grow?step=1')
+ post(f'/{app}/inputs/grow?step=1')
count *= 2
connection.close()
return count
@@ -113,10 +117,11 @@ def grow(port, items):
def main():
serve, items, changes, restarts = Path(sys.argv[1]).resolve(), int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
+ after = sys.argv[5] if len(sys.argv) > 5 else 'items-next'
work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm'))
try:
modules = {}
- for name in ('items', 'items-next'):
+ for name in ('items', after):
modules[name] = work / f'{name}.pmod'
subprocess.run([str(AUP), 'build', str(HERE / f'{name}.aup'), '-o', str(modules[name])], check=True)
conf, store, port = work / 'apps.conf', work / 'store', free_port()
@@ -132,17 +137,19 @@ def main():
time.sleep(0.5)
gaps = {'changed': [], 'restarted': []}
for n in range(changes):
- write('items-next' if n % 2 == 0 else 'items')
+ write(after if n % 2 == 0 else 'items')
since = time.monotonic()
began = time.perf_counter()
said = host.reading()
took = time.perf_counter() - began
- assert said['apps'][0]['did'] == 'restarted' and said['apps'][0]['running'], said
+ assert said['apps'][0]['did'] in ('restarted', 'changed') and said['apps'][0]['running'], said
time.sleep(0.3)
gaps['changed'].append(writer.gap(since))
- print(json.dumps({'items': count, 'way': 'changed', 'gap_ms': round(gaps['changed'][-1] * 1000, 2),
+ print(json.dumps({'items': count, 'way': 'changed', 'to': after if n % 2 == 0 else 'items',
+ 'did': said['apps'][0]['did'], 'gap_ms': round(gaps['changed'][-1] * 1000, 2),
'reading_ms': round(took * 1000, 2)}), flush=True)
for n in range(restarts):
+ write(after if (changes + n) % 2 == 0 else 'items')
since = time.monotonic()
host.stop()
host = Host(serve, conf, port)