perf(usage): cache empty macOS keychain scans for 30s - #22
Conversation
Each status line repaint is a new process, so an empty keychain scan repeated on every render. Remember the empty result in a cache file for 30s while still checking the credentials file and exact-token paths every scan, so direct credential changes stay live. Written by Codex; committed as-is. Co-Authored-By: Claude Code <noreply@anthropic.com>
axisrow
left a comment
There was a problem hiding this comment.
Обзор
PR кэширует пустой результат sweep хешированных keychain-кандидатов macOS на 30 секунд через пустой файл ~/.cache/ccstatusline/keychain-fallback-empty (читается только mtime). Дифф проверен против base main (74ba67f): один коммит b3019b9, 3 файла, +68 строк. Вердикт: approving — запрашивающих изменений нет.
Корректность
- Кэш затрагивает ровно один путь — readUsageCredentialsFromMacKeychainCandidates(), достигаемый только для профиля по умолчанию на darwin после промаха по plain-сервису. Plain-сервис Claude Code-credentials, профильный хешированный сервис и .credentials.json по-прежнему проверяются на каждом рендере — заявление из описания PR соответствует коду.
- TTL-логика верна: age >= 0 отбрасывает mtime из будущего (скачок часов), ровно 30 000 ms считается истёкшим. mtime обновляется только фактически выполненным sweep, поэтому 30 s остаётся честной верхней границей, а не растягивается бесконечно частыми репайнтами.
- Запись best-effort и обёрнута в try/catch вместе с ensureCacheDirExists(): read-only кэш-директория не ломает fallback на credentials-file.
- Кэш-файл пустой, содержимое никогда не парсится (только statSync), поэтому конкурентные записи параллельных процессов репайнта безопасны; секретов файл не содержит.
- Профильный путь (CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR) sweep не вызывает вовсе, так что кэш на мультипрофильные сценарии не влияет.
Трейдофф задокументирован
Новый login, существующий только в хешированном keychain-сервисе, может обнаруживаться с задержкой до 30 s. Это осознанное решение, отражённое и в комментарии кода, и в описании PR; для статусбара приемлемо.
Тесты
Новый тест покрывает окно переиспользования (29 999 ms), истечение (30 000 ms), защиту от mtime в будущем (шаг назад на 1 ms) и живость credentials-file и plain-сервиса при свежем кэше (dump-keychain остаётся на 3 вызовах). Моки statSync/writeFileSync/mkdirSync в beforeEach делают тесты герметичными относительно реального ~/.cache/ccstatusline — это же снимает потенциальный флейк существующего теста в usage-token-buffer.test.ts, который иначе мог бы зацепить реальный miss-файл разработчика.
Единственное замечание — не блокирующее (см. inline): в usage-token-buffer.test.ts замокан только read-side кэша; write-side сейчас недостижим, но на будущее не защищён.
| mockedExecFileSync.mockReset(); | ||
| mockedExecFileSync.mockImplementation(realExecFileSync); | ||
| vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin'); | ||
| vi.spyOn(fs, 'statSync').mockImplementation(() => { throw new Error('cache missing'); }); |
There was a problem hiding this comment.
Неблокирующее: замокан только read-side кэша (statSync). Сейчас write-путь здесь недостижим — единственный тест находит кандидата, — но будущий тест на full-miss записал бы реальный файл в ~/.cache разработчика. Стоит замокать writeFileSync и mkdirSync так же, как в usage-token.test.ts.
axisrow
left a comment
There was a problem hiding this comment.
Review: PR #22 — perf(usage): cache empty macOS keychain scans for 30s
Вердикт: Request changes — не из-за дефектов самого diff'а (код корректен, я готов апрувить его по существу), а из-за красного обязательного чека: lint падает на 21 ошибке, унаследованной с main. Пока main не поправлен (или фикс не включён в этот PR), merge-гейт не зелёный ни для одного PR из main.
Верификация негативного кэша (опасный класс изменений) — чисто
- Разделение профилей (
src/utils/usage-fetch.ts:612-631): miss-кэш живёт только вreadUsageCredentialsFromMacKeychainCandidates()(usage-fetch.ts:552), которая достижима только для дефолтного профиля. Не-дефолтные профили выходят рано черезconfigDirService(usage-fetch.ts:624) и кэша не касаются. Кэш возвращаетnull(отсутствие), а не чужой результат — перепутать профили невозможно архитектурно. - Фингерпринт по refresh token (sirmalloc#536): негативный кэш не хранит никаких аккаунт-данных (0-байтовый файл, содержимое никогда не читается — только
statSyncmtime), поэтому фингерпринт-гейт дата-кэша (usage-fetch.ts:640) его не касается и наоборот. Инвалидация по logout/login для точного сервиса (Claude Code-credentials) и.credentials.jsonсохраняется — они проверяются на каждом вызове, до/вокруг кэшированного fast-return. - TTL (
usage-fetch.ts:557-558): ровноage >= 0 && age < 30_000. mtime в будущем (отрицательный age) → перескан; протухший файл инертен; непустой скан файл не пишет вообще (write только после пустого прохода цикла,usage-fetch.ts:574-579). - Конкурентность: читается только mtime через
statSync, пишется пустая строка одним вызовом — нет вектора torn-read; худший случай гонки — дублирующий sweep, безвредно. Write best-effort, read-only~/.cacheдеградирует в обычный скан. - Решаринг sweep-находок между рендерами: если sweep что-то нашёл — файл не пишется, следующий рендер сканирует заново; ошибочного «дедупа обязательного скана» нет. Отложенное обнаружение (≤30s) возможно только для sweep-дискавери нестандартных сервисов — это осознанный трейдофф, задокументированный в комментарии
usage-fetch.ts:555.
Находки
1. Блокер мержа (не вина diff'а): lint красный на main
CI «Lint & Type Check» падает с 21 ошибкой, все — в файлах, которые PR не трогает: PowerlineThemeSelector.tsx (6), PowerlineThemeSelector.test.ts (2), renderer-regular-theme.test.ts (5), separator-font-fallback.test.ts (1), cli.ts (2), powerline.ts (1), renderer.ts (4). Локальный прогон на HEAD PR воспроизвёл те же 21 ошибку один-в-один; в трёх файлах диффа (usage-fetch.ts, оба usage-теста) ошибок нет — diff линт-чистый. Вывод: main сломан для линта ещё до этого PR (наследие тем-коммитов), и это блокирует merge-гейт всем PR. 19 из 21 чинятся bun run lint:fix. Варианты: отдельный chore(lint)-коммит в main до мержа, либо включить в этот PR — на усмотрение автора.
2. Should-fix: write-путь не замокан в usage-token-buffer.test.ts:38
Поддерживаю комментарий @axisrow (PRRT_kwDOUqBlWc6mTGSl): сейчас замокан только statSync; сегодняшний тест находит кандидата и write-путь недостижим, но будущий тест на full-miss запишет реальный файл в ~/.cache разработчика. Стоит добавить vi.spyOn(fs, 'writeFileSync') + vi.spyOn(fs, 'mkdirSync') так же, как в usage-token.test.ts. Неблокирующее само по себе, но дешёвое — логично включить в тот же фикс-пуш, что и п.1.
Прогоны
bun install— ок; целевые тесты трёх затронутых файлов: 15 pass / 0 fail (93 expect).- CI Test на PR — зелёный (оба прогона); заявленное покрытие реально: новый тест в
usage-token.test.ts:248закрывает 30s-окно, границу истечения, отрицательный age (mtime в будущем), приоритет.credentials.jsonнад свежим кэшем и точного сервиса — без лишнего sweep. - Локальный полный
bun testдал флейки вcustom-command-process.test.tsиRefreshIntervalMenu.test.ts— файлы, не затронутые PR, тайминги под локальной нагрузкой; CI те же тесты проходит. bun run lint: см. находку 1.
Смоук на реальном macOS keychain
Выполнен на этой машине (её живой статус-лайн уже работает на коммите b3019b9): miss-файл реально живёт в ~/.cache/ccstatusline/, механизм подтверждён — вызов на протухшем кэше выполняет полный sweep, вызов на свежем его пропускает (11550ms → 429ms; абсолютные числа загрязнены параллельным tsc, автономный security dump-keychain здесь стоит ~0.63s). Креденшлы не печатались и не кэшировались. Дополнительных зависимостей нет, подавлений линта нет.
Итог
Сам diff: корректный, минимальный, покрыт тестами — по существу это approve. Request changes ставлю только из-за красного обязательного lint-чека (находка 1); после его снятия п.2 можно добить тем же пушем.
axisrow
left a comment
There was a problem hiding this comment.
Review pass 2: PR #22 @ fd9f945 (merge of b35114c into fix/keychain-miss-cache)
Вердикт: approving. Дифф против нового base идентичен уже полностью
проверенному b3019b9 (3 файла, +68: usage-fetch.ts + два usage-теста) —
корректность негативного кэша подтверждена предыдущим проходом: профильная
изоляция (кэш достижим только для дефолтного профиля), честный TTL 30с с
защитой от mtime в будущем, запись только после пустого sweep, секреты не
кэшируются, гонки безвредны, fingerprint дата-кэша не затронут.
Блокер первого прохода (красный «Lint & Type Check» из-за долга main) снят:
фиксы ушли в main через #21, ветка обновлена, на этой голове все чеки
зелёные (Lint/Test/Build ×2, runs 36287843…/36287846…). В файлах диффа
lint-ошибок нет.
Неблокирующее, остаётся открытым (тред PRRT_kwDOUqBlWc6mTGSl):
в src/utils/__tests__/usage-token-buffer.test.ts:38 замокан только
read-side кэша (statSync); будущий тест на full-miss записал бы реальный
файл в ~/.cache разработчика. Стоит добавить моки writeFileSync/mkdirSync
по образцу usage-token.test.ts — удобно добрать отдельным мелким ПРом.
Follow-up to the #22 review (PRRT_kwDOUqBlWc6mTGSl): only the read side (statSync) was mocked, so a future full-miss test would write a real marker into the developer's ~/.cache. Mock writeFileSync and mkdirSync like usage-token.test.ts does, and add the miss-branch test: a dump-keychain scan finding nothing must record the keychain-fallback-empty marker and still yield no token. Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
Cache empty macOS keychain scans for 30 seconds
Every status line repaint is a new ccstatusline process, so on macOS an empty keychain scan (locked keychain / no credentials stored) was repeated from scratch on every render of the usage widgets.
This remembers an empty scan in a small cache file (
keychain-fallback-empty) for 30 seconds. The credentials file and exact-token paths are still checked on every scan, so direct credential changes stay live; only the expensive candidate sweep is short-circuited. The cache write is best-effort — a read-only cache dir falls back to scanning normally.Includes a regression test covering the 30s reuse window, expiry, and credentials-file changes overriding a cached miss.
Written by Codex; committed as-is.
🤖 Generated with Claude Code