Skip to content

perf(#257): elapsed を Presenter 側で補間し helper Timer を 3 秒に延長#264

Merged
GeneralD merged 6 commits into
mainfrom
perf/257/elapsed-interpolation-helper-3s
Jun 1, 2026
Merged

perf(#257): elapsed を Presenter 側で補間し helper Timer を 3 秒に延長#264
GeneralD merged 6 commits into
mainfrom
perf/257/elapsed-interpolation-helper-3s

Conversation

@GeneralD

@GeneralD GeneralD commented Jun 1, 2026

Copy link
Copy Markdown
Owner

type breaking scope tests review

Closes #257

背景

#263media-remote-helper.swift を swiftc 事前コンパイルから swift interpret モードに巻き戻したため、helper のホストプロセスは swift-frontend -interpret (Apple 署名) の常駐となった。これにより 1 秒毎の polling Timer のコストが「軽量バイナリ × 高頻度」から「interpreter ホスト × 高頻度」に変化し、idle 時の常駐コストが以前より重くなった。

MediaRemote が返す timestampplaybackRate から elapsed は client 側で完全に補間できる:

elapsed_now = base_elapsed + playbackRate * (now - timestamp)

つまり polling 間隔を延ばしても、補間さえ正しく行えば歌詞同期に支障は出ない。pause/seek は MediaRemote の kMRMediaRemoteNowPlayingInfoDidChangeNotification notification で即時反映されるため、Timer は「補間の基準点を再取得する役」だけに専念できる。

変更内容

Entity / Interactor

  • PlaybackPosition(elapsed, playbackRate) から純粋スナップショット (rawElapsed, timestamp, playbackRate)変更
  • TrackInteractorImpl.playbackPosition は補間せず raw データを passthrough するだけに

Presenter

  • LyricsPresenter@Dependency(\.date.now) を持つ
  • スナップショット (latestRawElapsed / latestTimestamp / latestPlaybackRate) を保持
  • updateActiveLineTick (DisplayLink 60fps) で毎回 base + rate * (now - timestamp) を計算
  • timestamp が nil の場合は rawElapsed をそのまま使う fallback path

Helper

  • Timer.scheduledTimer の間隔を 1.03.0 秒に延長
  • コメントで「snapshot 用、補間は client 側」と明記

保持

  • PlaybackUseCase.elapsedTime(for:)TrackHandler (CLI lyra track) で使われているため温存

テスト

新規 2 件:

テスト 検証内容
interpolates elapsed from snapshot (rawElapsed + timestamp + playbackRate) \.date.now を固定値に override し、7 秒前の timestamp で rawElapsed=0, rate=1.0 を送ると updateActiveLineTickelapsed=7 扱いになり Line B (time=5) が highlight される
falls back to rawElapsed when timestamp is nil timestamp 欠落時は補間せず rawElapsed をそのまま使う

既存 18 件は init 引数を elapsed:rawElapsed:置換のみで pass。全 909 tests pass。

トレードオフ

項目 旧 (1s Timer + 補間あり) 新 (3s Timer + 毎フレーム補間)
歌詞ハイライト精度 1 秒ズレ (tick で再計算しない) フレーム単位精度
pause/seek 検知 notification で即時 notification で即時 (変化なし)
helper polling コスト 1 Hz 0.33 Hz (3 倍削減)
swift-frontend host 負荷

歌詞精度はむしろ 向上 している (補間が tick 毎に効くため)。

関連

Summary by CodeRabbit

  • Chores

    • Version bumped to 2.13.7.
  • Refactor

    • Playback position now carries raw elapsed plus an optional timestamp and supports interpolation from that snapshot using current time.
    • Now-playing snapshot handling always supplies a timestamp and reduces the periodic refresh rate to fewer polls.
  • Tests

    • Updated and added tests covering snapshot fields, nil-handling, emission ordering, suppression patterns, race conditions, and interpolation behavior.

GeneralD added 2 commits June 1, 2026 16:25
PlaybackPosition Entity を rawElapsed + timestamp + playbackRate の純粋スナップショットに変更し、
LyricsPresenter.updateActiveLineTick で `\.date.now` を使って毎フレーム補間する。
これにより helper の polling 間隔を 1s → 3s に延長しても歌詞ハイライトのズレは出ない
(DisplayLink tick 毎に最新の補間値が計算されるため)。

#263 で swift-interpret モードに戻して helper のホストが swift-frontend 常駐になった
文脈で、polling 削減の idle CPU 効果が以前より大きい。pause/seek は MediaRemote の
notification (kMRMediaRemoteNowPlayingInfoDidChange) で即時反映される。
Copilot AI review requested due to automatic review settings June 1, 2026 07:26
@GeneralD GeneralD self-assigned this Jun 1, 2026
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1602dc84-2b62-4251-8081-5f88eebd8dce

📥 Commits

Reviewing files that changed from the base of the PR and between fa8a663 and 78acf8b.

📒 Files selected for processing (5)
  • Tests/PresentersTests/LyricsPresenterDuplicateTests.swift
  • Tests/PresentersTests/LyricsPresenterTests.swift
  • Tests/TrackInteractorTests/TrackInteractorPlaybackPositionTests.swift
  • Tests/TrackInteractorTests/TrackInteractorRaceTests.swift
  • Tests/TrackInteractorTests/TrackInteractorStyleTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/PresentersTests/LyricsPresenterTests.swift

📝 Walkthrough

Walkthrough

PlaybackPosition now includes snapshot fields (rawElapsed, timestamp); sources emit snapshots; LyricsPresenter interpolates elapsed from snapshots using injected now; media-remote helper always supplies a timestamp and polls every 3s; tests updated and new tests added for propagation and interpolation.

Changes

Playback Position Interpolation for Lyric Sync

Layer / File(s) Summary
PlaybackPosition contract refactoring
Sources/Entity/PlaybackPosition.swift
elapsed renamed to rawElapsed, added timestamp: Date?, initializer updated to init(rawElapsed:timestamp:playbackRate:), and Equatable conformance added.
Source data providers
Sources/TrackInteractor/TrackInteractorImpl.swift, Sources/AppRouter/AppDependencyBootstrap.swift
TrackInteractor and UITest bootstrap now construct PlaybackPosition from NowPlaying's rawElapsed, timestamp, and playbackRate.
LyricsPresenter interpolation implementation
Sources/Presenters/Track/LyricsPresenter.swift
Stores latestRawElapsed and latestTimestamp, injects date.now, and computes interpolatedElapsed = rawElapsed + playbackRate * (now - timestamp) inside updateActiveLineTick() to drive active lyric selection.
Media-remote polling optimization
Sources/MediaRemoteDataSource/Resources/media-remote-helper.swift
When timestamp is missing, synthesize Date() for timestamp; increase fallback fetchAndPrint() timer from 1.0s to 3.0s and update explanatory comment.
Presenter tests migration & interpolation coverage
Tests/PresentersTests/LyricsPresenterTests.swift, Tests/PresentersTests/LyricsPresenterDuplicateTests.swift
Updated tests to use rawElapsed; added tests that assert interpolation when timestamp is present and fallback when timestamp is nil; duplicate tests updated accordingly and polling loops used for tick verification.
TrackInteractor tests, race & style checks
Tests/TrackInteractorTests/*
New TrackInteractorPlaybackPositionTests with stubs and PositionCollector; added volume-mute suppression test, a race-condition test for nil title/artist, and style property tests verifying AppStyle propagation.
Version bump
Sources/VersionHandler/Resources/version.txt
Repository version incremented from 2.13.6 to 2.13.7.

Sequence Diagram

sequenceDiagram
  participant MediaRemote
  participant TrackInteractor
  participant LyricsPresenter
  participant LyricsView
  MediaRemote->>TrackInteractor: NowPlaying(rawElapsed, timestamp?, playbackRate) (periodic / notification)
  TrackInteractor->>LyricsPresenter: PlaybackPosition(rawElapsed, timestamp?, playbackRate)
  activate LyricsPresenter
  LyricsPresenter->>LyricsPresenter: store latestRawElapsed & latestTimestamp
  LyricsPresenter->>LyricsPresenter: interpolatedElapsed = rawElapsed + playbackRate * (now - timestamp)
  LyricsPresenter->>LyricsView: update activeLineIndex
  MediaRemote->>TrackInteractor: kMRMediaRemoteNowPlayingInfoDidChange (pause/seek)
  TrackInteractor->>LyricsPresenter: immediate PlaybackPosition update
  deactivate LyricsPresenter
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • GeneralD/lyra#187: Updates around LyricsPresenter.updateActiveLineTick and related presenter tests; overlaps in presenter-test changes.

Poem

🐰 I sniff raw ticks and a timestamp fine,
I hop between snapshots to keep each line in time.
Three seconds now, the helper lets me rest—
I mend the beats and make the lyrics best. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: interpolating elapsed time on the presenter side and extending the helper timer interval from 1.0s to 3.0s, matching the primary objective of issue #257.
Linked Issues check ✅ Passed The pull request implements all coding requirements from issue #257: client-side interpolation in LyricsPresenter, helper timer interval changed to 3.0s, PlaybackPosition API updated to include rawElapsed and timestamp, and test coverage added for interpolation behavior.
Out of Scope Changes check ✅ Passed All changes directly support the objectives in issue #257; however, version.txt was updated (2.13.6 → 2.13.7) which is tangential to the core performance objective, though the PR objectives confirm this was intentionally included.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/257/elapsed-interpolation-helper-3s

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces idle cost of the MediaRemote helper by extending its polling interval while preserving (and improving) lyric highlight accuracy by interpolating elapsed time on the client side (Presenter) using (rawElapsed, timestamp, playbackRate) snapshots.

Changes:

  • Change PlaybackPosition to a snapshot model (rawElapsed, timestamp, playbackRate) and pass it through from TrackInteractorImpl.
  • Interpolate elapsed every DisplayLink tick in LyricsPresenter using an injected date.now.
  • Extend media-remote-helper.swift Timer polling from 1s to 3s and add presenter tests for interpolation + timestamp-nil fallback; bump version to 2.13.7.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Tests/PresentersTests/LyricsPresenterTests.swift Updates existing tests to rawElapsed and adds new tests validating interpolation + nil-timestamp fallback.
Tests/PresentersTests/LyricsPresenterDuplicateTests.swift Updates playback position construction to use rawElapsed.
Sources/VersionHandler/Resources/version.txt Bumps patch version to 2.13.7.
Sources/TrackInteractor/TrackInteractorImpl.swift Stops pre-interpolating elapsed in the interactor; publishes raw snapshot fields.
Sources/Presenters/Track/LyricsPresenter.swift Stores latest snapshot and interpolates elapsed per tick using @Dependency(\.date.now).
Sources/MediaRemoteDataSource/Resources/media-remote-helper.swift Extends polling interval to 3s and documents snapshot approach.
Sources/Entity/PlaybackPosition.swift Renames elapsedrawElapsed and adds timestamp to support client interpolation.
Sources/AppRouter/AppDependencyBootstrap.swift Updates UI-test dependency bootstrap to the new PlaybackPosition initializer.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +63
// Periodic fallback for snapshot refresh (rawElapsed/timestamp/playbackRate).
// The client (LyricsPresenter) interpolates elapsed on every DisplayLink tick
// from this snapshot, so 3s polling is sufficient for lyric sync. pause/seek
// is delivered immediately via `kMRMediaRemoteNowPlayingInfoDidChange`.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

指摘の通りです。コメント内の notification 名を kMRMediaRemoteNowPlayingInfoDidChangeNotification修正しました (2b7f7d9)。PR body の同じ箇所もあわせて更新しています。

Comment on lines +275 to +278
try? await Task.sleep(for: .milliseconds(50))

presenter.updateActiveLineTick()
#expect(presenter.activeLineIndex == 1)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

deadline polling パターン (3s) に置き換えました (2b7f7d9)。updateActiveLineTick を内側で繰り返し呼び activeLineIndex == 1 到達で break する形に揃え、CLAUDE.md "Async test timing" のガイドラインに準拠しました。

Comment on lines +309 to +312
try? await Task.sleep(for: .milliseconds(50))

presenter.updateActiveLineTick()
#expect(presenter.activeLineIndex == 1)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

deadline polling パターン (3s) に置き換えました (2b7f7d9)。updateActiveLineTick を内側で繰り返し呼び activeLineIndex == 1 到達で break する形に揃え、CLAUDE.md "Async test timing" のガイドラインに準拠しました。

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 090761dc7c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// The client (LyricsPresenter) interpolates elapsed on every DisplayLink tick
// from this snapshot, so 3s polling is sufficient for lyric sync. pause/seek
// is delivered immediately via `kMRMediaRemoteNowPlayingInfoDidChange`.
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in fetchAndPrint() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep frequent polling when timestamps are unavailable

With MediaRemote sources that omit kMRMediaRemoteNowPlayingInfoTimestamp, the presenter explicitly falls back to rawElapsed without interpolation (LyricsPresenter.interpolatedElapsed returns base when the timestamp is nil), so increasing the helper fallback to 3 seconds makes lyric highlighting update only once every 3 seconds for those sources. The previous 1-second fallback was the only mechanism advancing elapsed time in this scenario; either keep a shorter interval for timestamp-less snapshots or synthesize/update a timestamp before relying on the 3-second poll.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

指摘の懸念に対し、helper 側で fetch 時刻を timestamp として注入する形に修正しました (2b7f7d9)。

kMRMediaRemoteNowPlayingInfoTimestamp欠落した snapshot では Date() を代替値として使用し、client 側の補間が常に有効になるようにしています。これにより timestamp-less ソースでも 3 秒 polling 間隔のままフレーム単位精度を維持できます。

playbackRate == 0 (pause) 時は LyricsPresenter 側で補間自体をスキップする (updateActiveLineTick のガード) ため、停止中に誤って前進する副作用はありません。

rawElapsed: 0,
timestamp: fixedNow.addingTimeInterval(-7),
playbackRate: 1.0))
try? await Task.sleep(for: .milliseconds(50))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace fixed sleeps with deadline polling

This newly added test waits for Combine delivery with a fixed Task.sleep, and the repo's root AGENTS.md testing rules explicitly say not to wait on async state with fixed sleeps. On a slower or busier CI worker the publisher delivery can exceed 50 ms and make the assertion flaky; poll the expected presenter state until a deadline as the surrounding tests already do.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

deadline polling パターン (3s) に置き換えました (2b7f7d9)。updateActiveLineTick を内側で繰り返し呼び activeLineIndex == 1 到達で break する形に揃え、CLAUDE.md "Async test timing" のガイドラインに準拠しました。

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/VersionHandler/Resources/version.txt (1)

1-2: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove unintended trailing 2 from version resource.

The file now contains two lines; many version readers expect exactly one SemVer string and may fail or read an invalid value.

Suggested fix
 2.13.7
-2
🤖 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 `@Sources/VersionHandler/Resources/version.txt` around lines 1 - 2, The version
resource currently has an unintended extra line containing "2"; edit version.txt
so it contains exactly one line with the SemVer string "2.13.7" (remove the
stray second-line "2" and any trailing blank lines) so version readers will
parse a single valid version value.
🤖 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 `@Tests/PresentersTests/LyricsPresenterTests.swift`:
- Around line 268-313: Replace the racy try? await Task.sleep(...) calls in the
LyricsPresenterTests (the interpolation test block and the
fallsBackWhenTimestampMissing test) with a poll-until-deadline loop that
repeatedly checks for the expected state instead of sleeping: after sending the
PlaybackPosition to the positionSubject, poll until presenter.activeLineIndex ==
1 (or until a short deadline like 500ms) before calling
presenter.updateActiveLineTick(), failing the test if the deadline is exceeded;
update both occurrences that currently call Task.sleep so they mirror the
existing wait/poll pattern used elsewhere (you can reuse the same polling helper
used by waitForLyricsSuccess) to ensure the Combine sink has delivered the
position.

---

Outside diff comments:
In `@Sources/VersionHandler/Resources/version.txt`:
- Around line 1-2: The version resource currently has an unintended extra line
containing "2"; edit version.txt so it contains exactly one line with the SemVer
string "2.13.7" (remove the stray second-line "2" and any trailing blank lines)
so version readers will parse a single valid version value.
🪄 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

Run ID: 835d71f2-d7f3-4653-b6c1-a8eaf2a4cf09

📥 Commits

Reviewing files that changed from the base of the PR and between c201e64 and 090761d.

📒 Files selected for processing (8)
  • Sources/AppRouter/AppDependencyBootstrap.swift
  • Sources/Entity/PlaybackPosition.swift
  • Sources/MediaRemoteDataSource/Resources/media-remote-helper.swift
  • Sources/Presenters/Track/LyricsPresenter.swift
  • Sources/TrackInteractor/TrackInteractorImpl.swift
  • Sources/VersionHandler/Resources/version.txt
  • Tests/PresentersTests/LyricsPresenterDuplicateTests.swift
  • Tests/PresentersTests/LyricsPresenterTests.swift

Comment thread Tests/PresentersTests/LyricsPresenterTests.swift
Codex P2 / Copilot レビュー指摘への対応。

- helper: MediaRemote が timestamp を返さないソース向けに、fetch 時刻
  を timestamp として注入。これがないと client 側の補間が
  base (rawElapsed そのもの) に縮退し、3 秒 polling 間隔のまま歌詞
  ハイライトが追従しなくなる。playbackRate=0 (pause) では補間自体を
  スキップするため副作用なし。
- helper: コメント内の notification 名を
  kMRMediaRemoteNowPlayingInfoDidChange →
  kMRMediaRemoteNowPlayingInfoDidChangeNotification に修正
  (Apple 公式の正式名にあわせる)。
- tests: 新規追加した 2 件 (interpolatesFromSnapshot /
  fallsBackWhenTimestampMissing) で使っていた固定 Task.sleep(50ms)
  を deadline polling (3s) に置換。CLAUDE.md "Async test timing" の
  ガイドラインに準拠し、CI 負荷で flaky 化するリスクを除去。
@GeneralD

GeneralD commented Jun 1, 2026

Copy link
Copy Markdown
Owner Author

action

@coderabbitai Outside-diff の Sources/VersionHandler/Resources/version.txt 指摘について検証しました。実ファイルは 7 bytes の 1 行のみで、末尾 2混入はありません。

$ xxd Sources/VersionHandler/Resources/version.txt
00000000: 322e 3133 2e37 0a                        2.13.7.
$ wc -l Sources/VersionHandler/Resources/version.txt
       1

wc -l1 を返すのは POSIX 改行終端 (\n) で正常です。加えて読み出し側の VersionHandlerImpl.swift:13String(contentsOf:).trimmingCharacters(in: .whitespacesAndNewlines)適用しているため、末尾改行・空白は無害です。誤検出と判断し、ファイルは現状維持とします。

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

@GeneralD ご検証ありがとうございます。xxd の出力から 2.13.7\n(7 bytes)のみであること、VersionHandlerImpl.swift:13trimmingCharacters(in: .whitespacesAndNewlines) で末尾改行が無害に処理されることが確認できました。誤検出でした。ご指摘の通り、ファイルは現状維持で問題ありません。

(´・ω・`) \(゚ヮ゚)/ 誤検出スミマセン!


✏️ Learnings added
Learnt from: GeneralD
URL: https://github.com/GeneralD/lyra/pull/264

Timestamp: 2026-06-01T08:18:36.647Z
Learning: In the GeneralD/lyra repository, `Sources/VersionHandler/Resources/version.txt` is a plain POSIX text file with a single trailing newline (`\n`). The reader `VersionHandlerImpl.swift` (line 13) applies `.trimmingCharacters(in: .whitespacesAndNewlines)` when reading this file, so trailing whitespace or newlines are harmless. Do not flag trailing newlines in this file as an issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Actions performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

本 PR で変更した TrackInteractorImpl.playbackPosition publisher
(lines 64-69) がテストでカバーされておらず Codecov patch coverage が
失敗していたため、専用テスト suite を追加。

検証内容:
- rawElapsed / timestamp / playbackRate を NowPlaying からそのまま
  PlaybackPosition に passthrough すること
- nil rawElapsed / nil timestamp / rate=0 (pause) でも例外なく流れること
- NowPlaying が連続して変化した際に複数 snapshot を emit すること

既存の TrackInteractorArtworkTests と同じパターン (Stub + Collector +
makeInteractor helper) に揃え、suite は .serialized で安定性を担保。

@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 `@Tests/TrackInteractorTests/TrackInteractorPlaybackPositionTests.swift`:
- Around line 148-153: The test indexes snapshots[0] and snapshots[1] after a
non-fatal check (`#expect`(snapshots.count == 2)), which can still crash; change
that to a hard precondition so failures are diagnostic: replace the non-fatal
expect with a hard requirement (for example use `#require`(snapshots.count == 2)
or a guard like guard snapshots.count >= 2 else { XCTFail(...); return }) before
accessing snapshots[0] and snapshots[1] in TrackInteractorPlaybackPositionTests
(use the local variables collector and snapshots to locate the code).
🪄 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

Run ID: a3ddaf23-6a74-4771-b3ee-d8263bdb740c

📥 Commits

Reviewing files that changed from the base of the PR and between 090761d and fa8a663.

📒 Files selected for processing (3)
  • Sources/MediaRemoteDataSource/Resources/media-remote-helper.swift
  • Tests/PresentersTests/LyricsPresenterTests.swift
  • Tests/TrackInteractorTests/TrackInteractorPlaybackPositionTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/PresentersTests/LyricsPresenterTests.swift

PR の主目的 (line 64-69 のカバー) を超えるが、関連する周辺コードの
未カバー領域もまとめて潰す。残り 4 lines は cancellation race
(`Task.isCancelled`) の経路で時序的に強制できないため対象外。

追加内容:

- volume-mute pattern (line 30)
  TrackInteractorPlaybackPositionTests に test 追加。
  「同一 title で artist が消失 → 抑制」を 3 つの emission 列で検証。
- computed style properties (lines 84-94)
  TrackInteractorStyleTests を新規追加。
  decodeEffectConfig / textLayout / artworkStyle の 3 つを
  非デフォルト・デフォルト両方で検証。
- nil title/artist guard (line 123)
  TrackInteractorRaceTests に test 追加。
  resolveTrack の guard で Just(loading) に短絡することと、
  以後 resolved/notFound が emit されないことを検証。

結果:
- lines coverage 92.94% → 97.49% (+4.55pt)
- function coverage 71.05% → 81.58%
- region coverage 66.67% → 75.76%

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment on lines 195 to 199
// Send a paused playback position (rate = 0)
positionSubject.send(PlaybackPosition(elapsed: 6, playbackRate: 0))
positionSubject.send(PlaybackPosition(rawElapsed: 6, playbackRate: 0))
// Allow Combine to deliver the position update
try? await Task.sleep(for: .milliseconds(50))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

deadline polling に置換しました (78acf8b)。1 秒間 updateActiveLineTick を polling し、activeLineIndex が nil 以外に flip したら早期 break する構造に変更したため、paused-guard の破れがあれば即座に検知できます。

Comment on lines 231 to 235
// Send position at 6s — should highlight Line B (time=5)
positionSubject.send(PlaybackPosition(elapsed: 6, playbackRate: 1.0))
positionSubject.send(PlaybackPosition(rawElapsed: 6, playbackRate: 1.0))
// Allow Combine to deliver the position update
try? await Task.sleep(for: .milliseconds(50))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

deadline polling に置換しました (78acf8b)。3 秒以内に activeLineIndex == 1到達すれば即 break する形で、Combine 配信の遅延に強くしています。

Comment on lines 159 to 167
// Set position while playing
positionSubject.send(PlaybackPosition(elapsed: 6.0, playbackRate: 1.0))
positionSubject.send(PlaybackPosition(rawElapsed: 6.0, playbackRate: 1.0))
try? await Task.sleep(for: .milliseconds(200))
presenter.updateActiveLineTick()
#expect(presenter.activeLineIndex == 1)

// Pause (rate = 0), send new position
positionSubject.send(PlaybackPosition(elapsed: 6.0, playbackRate: 0))
positionSubject.send(PlaybackPosition(rawElapsed: 6.0, playbackRate: 0))
try? await Task.sleep(for: .milliseconds(200))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

action

200ms 固定 sleep を 2 箇所とも deadline polling に置換しました (78acf8b)。rate=1.0 の活性化は 3 秒 polling で activeLineIndex == 1 到達まで待ち、rate=0 後は 1 秒 polling で「逸脱したら早期 break」する形に変更。paused-guard挙動を fast-fail で観測できる構造になりました。

…re に強化

CodeRabbit / Copilot レビュー指摘への対応。

### Copilot: 固定 Task.sleep 撤廃 (CLAUDE.md "Async test timing" 準拠)

3 箇所で残っていた固定 sleep を deadline polling に置換:

- LyricsPresenterTests `skips update when playback rate is 0 (paused)`
  Task.sleep(50ms) → 1s deadline で activeLineIndex==nil を polling 観測
- LyricsPresenterTests `updates active line index for timed lyrics`
  Task.sleep(50ms) → 3s deadline で activeLineIndex==1 になるまで polling
- LyricsPresenterDuplicateTests `pausedPlaybackKeepsIndex`
  Task.sleep(200ms) × 2 → それぞれ 3s / 1s deadline polling に。
  rate=1.0 配信は activeLineIndex==1 になるまで、rate=0 後は逸脱したら
  early break する形で「paused-guard 破れ」を高速検知できるように。

### CodeRabbit: 配列 index アクセス前に #require で固める

snapshots[0] / snapshots[1] アクセスは #expect(count == N) だけでは
non-fatal で素通りし、count が想定より小さいと out-of-bounds で
test runner ごと落ちる可能性。 try #require に強化:

- TrackInteractorPlaybackPositionTests.emitsMultipleSnapshots (count==2)
- TrackInteractorPlaybackPositionTests.volumeMutePatternSuppressed (count==2)
@GeneralD GeneralD merged commit aa965a0 into main Jun 1, 2026
4 checks passed
@GeneralD GeneralD deleted the perf/257/elapsed-interpolation-helper-3s branch June 1, 2026 10:28
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.

NowPlayingRepository で elapsed を補間し、helper Timer 間隔を 3-5 秒に延長

2 participants