Skip to content

fix(frame): сделать владение scheduler атомарным - #157

Merged
lemone112 merged 2 commits into
mainfrom
fix/frame-scheduler-atomic
Jul 14, 2026
Merged

lemone112 merged 2 commits into
mainfrom
fix/frame-scheduler-atomic

Conversation

@lemone112

@lemone112 lemone112 commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Пользовательский результат

Общий frame-loop больше не рекурсирует, не вклинивает вложенный кадр в незавершённую транзакцию и не удерживает callback/DOM после terminal-сценариев даже при нарушающем контракт host-clock. Обычный путь сохраняет один native rAF на кадр для всех потребителей.

Контракт

Порядок фаз read → update → render, exact-once, немедленный off, ленивый start/stop и публичные типы не меняются. Синхронный requestFrame переводится через отслеживаемый асинхронный trampoline. Native rAF вызывается с корректным globalThis receiver, а injected scheduler остаётся receiver-free и не доверяет подменённому .call. cancelAll() атомарно завершает старых владельцев до исполнения нового цикла.

Доказательство

  • Hostile RED-матрица воспроизводит recursion, OOB reentry, nested aggregate compute, stale timers, receiver hijack и GC-retention
  • Exact tree 9ea8a9d: frame/hostile 109/109
  • Полный последовательный набор: 230 файлов, 3 480 тестов
  • Chromium/Firefox/WebKit: 99/99
  • GC-probe под --expose-gc доказывает освобождение rollback/off/once callback
  • Независимый immutable review exact tree: GO, P0–P3 = 0

Архитектура

  • Один callback identity является SSOT владельца reservation
  • Тот же identity гасит late и double delivery без дублирующих latch/token
  • Sync-host проходит через отменяемый async-trampoline
  • live даёт O(1) idle-решение; один in-place compactor сохраняет фазовые границы
  • off, once и rollback сразу обнуляют callback/teardown ссылки
  • Ошибка текущего async-reschedule демотирует clock; stale host не владеет новым reservation

Производительность

Структурный ceiling-гейт сохраняет один rAF/кадр для full, mini и cross-entry; массовый animate-path нейтрален. Независимый AB/BA-аудит Node 24 удалил non-Pareto tuple: финальный мономорфный object-shape быстрее tuple примерно на 19,8% в recurring N=1000 и на 10% в once-churn. Абсолютная correctness-цена terminal-путей остаётся микросекундной на 1000 владельцев и меньше 0,2% бюджета кадра 240 Гц.

Размер относительно merged main #158:

  • frame: 640 → 739 B gzip;
  • consumer animate: 11 844 → 11 943 B gzip, запас 57 B;
  • consumer animate/mini: 4 870 → 4 948 B gzip, запас 52 B;
  • лимиты не повышены, все сценарии PASS.

Риски и не-цели

  • Внешний host callback нельзя переносимо отменить; stale delivery делается инертной identity-проверкой.
  • Срез не чинит отдельно logical-time/C¹ interruption в spring solver и не меняет ownership CSS-каналов.
  • Byte-only упрощения, ухудшающие frame throughput или terminal cleanup, не принимаются.

Документация и выпуск

  • CHANGELOG.md описывает действующий новый контракт
  • Публичные типы семантически прежние; меняется только пояснение scheduler-механизма
  • Migration note не требуется

Гейты

  • pnpm check:static
  • pnpm build
  • pnpm test: 3 480/3 480
  • pnpm size
  • pnpm test:browser:all: 99/99
  • pnpm pack:smoke
  • pnpm pack:compat
  • pnpm bench:ceiling: structural PASS
  • Все server checks exact head 703b891
  • Все review threads разрешены

Связано с #152

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb34eae5-ac69-4c8f-9aaa-57c3ef670e5d

📥 Commits

Reviewing files that changed from the base of the PR and between 703b891 and 404f506.

📒 Files selected for processing (1)
  • test/frame-retention-gc.test.ts
📝 Walkthrough

Walkthrough

The shared frame loop now uses reservation identity, mutable subscriber entries, explicit scheduling lifecycle helpers, atomic cancellation, and immediate callback release. Tests cover scheduler edge cases, synchronized animation execution, rollback, cancellation, and garbage-collection retention.

Changes

Frame loop lifecycle

Layer / File(s) Summary
Reservation-based scheduler lifecycle
src/frame/index.ts
The scheduler now tracks live entries, reservations, fallback timers, compaction, recovery, rollback, and atomic teardown.
Scheduler contract and synchronization tests
test/frame-loop.test.ts, test/animate-main-lane.test.ts
Tests cover receiver behavior, synchronous or repeated deliveries, stale timers, handle=0, rollback, cancelAll, re-entrant subscriptions, and synchronized slot execution.
Callback retention probes
test/fixtures/frame-retention-gc.probe.ts, test/frame-retention-gc.test.ts
GC probes and an esbuild-based harness verify callback collection after rollback, off(), and once teardown.
Frame behavior changelog
CHANGELOG.md
The unreleased changelog records reservation ownership, cancellable scheduling, atomic teardown, and immediate callback and DOM-reference release.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Subscriber
  participant FrameLoop
  participant HostScheduler
  participant Teardown
  Subscriber->>FrameLoop: subscribe or update callback
  FrameLoop->>HostScheduler: reserve and schedule frame
  HostScheduler->>FrameLoop: deliver frame callback
  FrameLoop->>Subscriber: execute phase callbacks
  Subscriber->>FrameLoop: unsubscribe or cancelAll
  FrameLoop->>Teardown: release callbacks and DOM references
Loading

Possibly related PRs

  • Labpics-Team/lab-motion#143: Modifies the shared frame scheduler’s subscription bookkeeping, teardown, and requestFrame cancellation semantics.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately reflects the main change: atomic scheduler ownership for frame.
Description check ✅ Passed The description follows the template well and covers user result, contract, evidence, architecture, performance, risks, docs, and gates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/frame-scheduler-atomic

Comment @coderabbitai help to get the list of available commands.

@lemone112
lemone112 force-pushed the fix/frame-scheduler-atomic branch from a8dc1a8 to 957163f Compare July 14, 2026 06:56
@lemone112
lemone112 marked this pull request as ready for review July 14, 2026 07:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frame/index.ts`:
- Around line 133-168: Update the native requestAnimationFrame invocation in
schedule so it preserves globalThis as the receiver, using a direct
globalThis.requestAnimationFrame call or an equivalent call(globalThis, fire).
Keep the injected requestFrame and fallback scheduling behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d5d1478-205f-4177-883b-9392dfc9d4dc

📥 Commits

Reviewing files that changed from the base of the PR and between bc42aaf and 957163f.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/frame/index.ts
  • test/animate-main-lane.test.ts
  • test/fixtures/frame-retention-gc.probe.ts
  • test/frame-loop.test.ts
  • test/frame-retention-gc.test.ts

Comment thread src/frame/index.ts Resolved
@lemone112
lemone112 force-pushed the fix/frame-scheduler-atomic branch from 957163f to 703b891 Compare July 14, 2026 10:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/frame-retention-gc.test.ts (1)

23-26: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding a timeout to the subprocess call.

If the probe script ever hangs (e.g., waiting on an unresolved promise or GC not triggering as expected), this test can block the run indefinitely with no bound.

♻️ Proposed fix
     const output = execFileSync(process.execPath, ['--expose-gc', outfile], {
       cwd: ROOT,
       encoding: 'utf8',
+      timeout: 30_000,
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/frame-retention-gc.test.ts` around lines 23 - 26, Update the
execFileSync invocation in the subprocess test to include a finite timeout
option, ensuring a hung probe terminates instead of blocking the test run
indefinitely while preserving the existing cwd and encoding settings.
test/frame-loop.test.ts (1)

296-871: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a withFakeTimers helper to cut boilerplate.

The vi.useFakeTimers(); try { ... } finally { vi.useRealTimers(); } wrapper is repeated in ~15 tests across this block. A tiny helper would remove the duplication without risking unrelated tests (avoids reaching for a global beforeEach/afterEach, which could break any test elsewhere in the file that relies on real timers/microtasks).

♻️ Suggested helper
function withFakeTimers(run: () => void): void {
  vi.useFakeTimers();
  try {
    run();
  } finally {
    vi.useRealTimers();
  }
}

Then each test body becomes it('...', () => withFakeTimers(() => { ... }));.

Traced the scheduling/tick/rollback logic in this block against createFrameLoop (src/frame/index.ts:71-230) for several representative cases (synchronous double-callback collapsing, handle=0 fallback demotion, resubscribe-during-tick isolation, cancelAll atomicity, stale-host-after-cancelAll) — all assertions match the given source semantics; no functional issues found.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/frame-loop.test.ts` around lines 296 - 871, Extract a local
withFakeTimers helper in the test file that wraps vi.useFakeTimers(), executes a
callback, and always restores real timers in finally. Refactor the repeated
fake-timer tests in this block to invoke their bodies through this helper,
preserving each test’s existing assertions and avoiding global timer hooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/frame-loop.test.ts`:
- Around line 296-871: Extract a local withFakeTimers helper in the test file
that wraps vi.useFakeTimers(), executes a callback, and always restores real
timers in finally. Refactor the repeated fake-timer tests in this block to
invoke their bodies through this helper, preserving each test’s existing
assertions and avoiding global timer hooks.

In `@test/frame-retention-gc.test.ts`:
- Around line 23-26: Update the execFileSync invocation in the subprocess test
to include a finite timeout option, ensuring a hung probe terminates instead of
blocking the test run indefinitely while preserving the existing cwd and
encoding settings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a4484a2-db40-4124-b71a-848c9bbddf16

📥 Commits

Reviewing files that changed from the base of the PR and between 957163f and 703b891.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/frame/index.ts
  • test/animate-main-lane.test.ts
  • test/fixtures/frame-retention-gc.probe.ts
  • test/frame-loop.test.ts
  • test/frame-retention-gc.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • CHANGELOG.md
  • test/fixtures/frame-retention-gc.probe.ts
  • src/frame/index.ts
  • test/animate-main-lane.test.ts

@lemone112

Copy link
Copy Markdown
Contributor Author

CodeRabbit follow-up: добавлен жёсткий 30_000 ms timeout для GC subprocess (404f506), targeted test проходит. Refactor повторяющихся fake-timer блоков в helper не применён: это большой test-only churn без изменения контракта; явный try/finally оставляет timer-scope видимым в каждом фазовом сценарии.

@lemone112
lemone112 merged commit 4421eaf into main Jul 14, 2026
7 checks passed
@lemone112
lemone112 deleted the fix/frame-scheduler-atomic branch July 14, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant