Skip to content

refactor!: make IFrameProvider/ISampleProvider IDisposable and guard IPC providers post-dispose#2013

Merged
yuto-trd merged 4 commits into
mainfrom
yuto-trd/ipc-provider-idisposable
Jun 25, 2026
Merged

refactor!: make IFrameProvider/ISampleProvider IDisposable and guard IPC providers post-dispose#2013
yuto-trd merged 4 commits into
mainfrom
yuto-trd/ipc-provider-idisposable

Conversation

@yuto-trd

@yuto-trd yuto-trd commented Jun 25, 2026

Copy link
Copy Markdown
Member

Description

Stacked on #2012 (base = yuto-trd/ipc-provider-hardening). Gives the frame/sample provider abstractions a deterministic teardown contract and hardens the IPC implementations against use-after-dispose.

  • IFrameProvider / ISampleProvider now implement IDisposable. An in-flight prefetch could previously outlive the encode and surface as an UnobservedTaskException; consumers can now dispose the provider, which neutralizes the pending prefetch (observing any fault and releasing a native Pcm on the success path).
  • IpcFrameProvider / IpcSampleProvider: Dispose() is idempotent and attaches a continuation to the in-flight prefetch instead of blocking on a possibly-closing pipe. Public entry points (RenderFrame / Sample) now ObjectDisposedException.ThrowIf after dispose. The intentional success-path asymmetry between the two providers (managed IpcMessage vs native Pcm) is documented inline.
  • Beutl.FFmpegWorker.EncodingHandler wraps the providers in using so the worker tears them down deterministically.

Affected areas

  • Beutl.Extensibility (plugin abstractions)
  • Beutl.FFmpegIpc / Beutl.FFmpegWorker (media IPC boundary)

Breaking changes

IFrameProvider and ISampleProvider now extend IDisposable. Out-of-tree implementations must add a Dispose() method (it may be a no-op for stateless providers), and owners of a provider instance should dispose it. refactor!: + BREAKING CHANGE: footer present on the implementing commit.

Test plan

  • tests/Beutl.FFmpegIpc.Tests/IpcProviderContractTests.cs (+116): two Dispose tests that drop an in-flight, deterministically-faulted prefetch (gated on an IsPrefetchFaultedForTest() probe, no sleeps) under an UnobservedTaskException watcher; existing serve/seek/cancel coverage retained.
  • tests/Beutl.Extensions.AVFoundation.Tests/TestProviders.cs + EncodingCancellationTests.cs: test doubles updated for the new IDisposable member.
  • Validated regression-free in a full-solution integration build + test (this branch cherry-picked with the other FFmpeg branches): Beutl.FFmpegIpc.Tests 18/18 green.

Fixed issues / References

Summary by CodeRabbit

  • Bug Fixes

    • Improved cleanup during encoding so frame and sample sources are disposed safely, reducing the risk of delayed background errors.
    • Prevented operations from continuing after a provider has been disposed, making encoding and playback-related workflows more reliable.
    • Fixed handling of in-flight background work so faults are observed instead of surfacing later as unhandled task exceptions.
  • Documentation

    • Added clearer usage and disposal guidance for encoding and provider components.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The PR documents encoding/provider disposal rules, makes IPC frame and sample providers track disposed state and observe faulted prefetch work during teardown, disposes those providers in the worker handler, and adds tests that verify no unobserved task exceptions surface.

Changes

Encoding disposal and prefetch observation

Layer / File(s) Summary
Public disposal contracts
src/Beutl.Extensibility/EncodingController.cs, src/Beutl.Extensibility/IFrameProvider.cs, src/Beutl.Extensibility/ISampleProvider.cs
EncodingController docs now describe frame/sample ownership and disposal, IFrameProvider docs describe prefetch teardown, and ISampleProvider now inherits IDisposable.
Frame provider teardown
src/Beutl.FFmpegIpc/Providers/IpcFrameProvider.cs
IpcFrameProvider tracks disposal state, rejects RenderFrame after disposal, and observes faulted prefetch work during Dispose().
Sample provider teardown
src/Beutl.FFmpegIpc/Providers/IpcSampleProvider.cs
IpcSampleProvider tracks disposal state, rejects Sample after disposal, and observes faulted prefetch work during Dispose().
Handler ownership and test provider cleanup
src/Beutl.FFmpegWorker/Handlers/EncodingHandler.cs, tests/Beutl.Extensions.AVFoundation.Tests/EncodingCancellationTests.cs, tests/Beutl.Extensions.AVFoundation.Tests/TestProviders.cs
HandleStartAsync now disposes the IPC providers with using var, and the AVFoundation test providers add forwarding or no-op Dispose() methods.
IPC disposal contract tests
tests/Beutl.FFmpegIpc.Tests/IpcProviderContractTests.cs
The IPC contract tests add an unobserved-exception watcher and assert that disposing faulted frame and sample prefetches stays silent and idempotent.

Sequence Diagram(s)

sequenceDiagram
  participant EncodingHandler
  participant EncodingController
  participant IpcFrameProvider
  participant IpcSampleProvider
  EncodingHandler->>EncodingController: Encode(frameProvider, sampleProvider, outputFile)
  EncodingController-->>EncodingHandler: returns or throws
  EncodingHandler->>IpcFrameProvider: Dispose()
  EncodingHandler->>IpcSampleProvider: Dispose()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I hop through frames and samples bright,
and tidy up at end of night. 🐇
Faulted tasks no longer drift,
the prefetch winds get a careful lift,
and every encode lands just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 captures the main breaking API change and the IPC post-dispose guard behavior.
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.

✏️ 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 yuto-trd/ipc-provider-idisposable

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

@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: 83e4535808

ℹ️ 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".

Comment thread src/Beutl.Extensibility/IFrameProvider.cs
@yuto-trd yuto-trd force-pushed the yuto-trd/ipc-provider-idisposable branch 2 times, most recently from 6f0c57c to d304835 Compare June 25, 2026 05:35
Base automatically changed from yuto-trd/ipc-provider-hardening to main June 25, 2026 05:41
yuto-trd added 4 commits June 25, 2026 14:43
…inistic teardown

Encoder frame/sample providers can hold an in-flight background prefetch (the
IPC providers double-buffer the next frame/chunk). Without a disposal contract,
a faulted prefetch that nobody awaits surfaces later as an
UnobservedTaskException when the GC reclaims the task.

Make IFrameProvider and ISampleProvider extend IDisposable so providers can be
torn down deterministically, and migrate every implementer:

- IpcFrameProvider / IpcSampleProvider gain an idempotent Dispose() that
  *observes* (does not block on) the in-flight _prefetchTask via a
  fault-swallowing continuation, so a faulted prefetch is marked observed and
  Dispose returns promptly even if the pipe is already torn down (a synchronous
  .Wait() could deadlock/rethrow). IpcSampleProvider additionally disposes the
  cached _currentChunk and the Pcm a successfully-completing prefetch produces.
- EncodingHandler (GPL worker) now wraps both providers in `using` so they are
  disposed after Encode returns or throws.
- FrameProviderImpl / SampleProviderImpl / AudioBufferSampleProvider already
  implemented IDisposable and their owners already dispose them (OutputViewModel
  via `using`); FFmpegEncodingControllerProxy keeps NOT disposing the
  caller-owned providers (ownership stays with the app-side creators).
- Test doubles (GradientFrameProvider, SineSampleProvider, SweepSampleProvider,
  CancelAfterFirstFrameProvider) gain a Dispose().

Adds IpcProviderContractTests proving Dispose() observes a faulted in-flight
prefetch so no UnobservedTaskException attributable to the provider is raised
(frame and sample side), driven via internal IsPrefetchFaultedForTest probes.

BREAKING CHANGE: IFrameProvider and ISampleProvider in Beutl.Extensibility now
extend IDisposable. Out-of-tree implementers must implement IDisposable. If the
provider starts a background prefetch, Dispose() must drain (observe) it — e.g.
attach a fault-swallowing continuation that touches Task.Exception — so a
faulted prefetch cannot later raise an UnobservedTaskException; do not block on
the task synchronously. Stateless providers may use an empty Dispose().

Refs: Project #9 "AI Review" item 266.
…ment Dispose asymmetry

Address review findings on the IDisposable change:
- Throw ObjectDisposedException from IpcFrameProvider.RenderFrame and
  IpcSampleProvider.Sample after Dispose, matching the public FrameProviderImpl /
  SampleProviderImpl implementations now that the interfaces are IDisposable.
- Document why IpcFrameProvider.Dispose uses OnlyOnFaulted while IpcSampleProvider
  also disposes on success: the frame prefetch yields a managed IpcMessage (the
  Bitmap is built lazily on consumption, nothing to release), whereas the sample
  prefetch yields a native Pcm that must be disposed on the success path. The
  asymmetry is intentional, not an oversight — a reviewer read the frame prefetch
  as Task<Bitmap>; it is Task<IpcMessage>, so there is no Bitmap to leak.

Refs: Project #9 "AI Review" item 266
…Controller.Encode

Now that IFrameProvider/ISampleProvider are IDisposable, an EncodingController
implementation receives IDisposable arguments with no documented signal about
who owns them. Following the "last user disposes" heuristic and disposing a
provider inside Encode would tear down resources the caller still owns (e.g.
FrameProviderImpl.Dispose cancels the background producer task), so document on
the abstract Encode that the caller retains ownership and implementations must
not dispose either argument — closing the footgun the IDisposable change adds.

Also clarify IpcSampleProvider.Dispose: its continuation handles all three
terminal states (Faulted / CompletedSuccessfully / Canceled) and why
ExecuteSynchronously is safe (t.Result.Dispose() is a single P/Invoke free).

Docs/comments only; no logic or API change. Tests unchanged (18/18 green).
…mment

Drop the "is deliberate ... is intentional" reviewer-facing framing; keep the
substance — the frame prefetch holds a managed IpcMessage (Bitmap built lazily
on consume) so OnlyOnFaulted leaks nothing, contrasted with the sample
provider's native Pcm that its continuation disposes on success. Comment only.
@yuto-trd yuto-trd force-pushed the yuto-trd/ipc-provider-idisposable branch from d304835 to 50d7413 Compare June 25, 2026 05:44
@github-actions

Copy link
Copy Markdown
Contributor

No TODO comments were found.

@yuto-trd yuto-trd enabled auto-merge (squash) June 25, 2026 05:45
@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage

Package Line Rate Branch Rate Complexity Health
Beutl.Api 9% 4% 1171
Beutl.Configuration 41% 22% 350
Beutl.Controls 0% 0% 5513
Beutl.Core 62% 56% 3069
Beutl.Editor 78% 70% 1742
Beutl.Editor.Components 1% 0% 8552
Beutl.Embedding.MediaFoundation 6% 8% 1376
Beutl.Engine 63% 52% 18123
Beutl.Engine.SourceGenerators 59% 44% 540
Beutl.Extensibility 38% 59% 112
Beutl.Extensions.AVFoundation 5% 12% 246
Beutl.Extensions.FFmpeg 6% 7% 853
Beutl.FFmpegIpc 25% 33% 825
Beutl.Language 7% 50% 1349
Beutl.NodeGraph 24% 15% 2474
Beutl.ProjectSystem 58% 43% 1061
Beutl.Threading 100% 90% 137
Beutl.Utilities 94% 87% 358
Summary 35% (43575 / 124687) 31% (10858 / 35508) 47851

Minimum allowed line rate is 0%

@yuto-trd yuto-trd merged commit fb6afd6 into main Jun 25, 2026
3 of 4 checks passed
@yuto-trd yuto-trd deleted the yuto-trd/ipc-provider-idisposable branch June 25, 2026 05:49
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