Skip to content

Conversation

@tharkum
Copy link
Contributor

@tharkum tharkum commented Oct 29, 2025

The associated callbacks should capture weak references of required objects with which they are will interact otherwise it leads to leaked underlying gst resources on high level owner (Player) destruction due to the cyclic reference dependency (between PlayerInner, ServoSrc and the associated callbacks).

PlayerInner -> PlaySignalAdapter callbacks (strong -> weak refs)
ServoSrc -> ServoSrc callbacks (strong -> weak refs)

Testing: Manually testing with the following WPT test ./mach test-wpt html/semantics/embedded-content/the-video-element/intrinsic_sizes.htm -r with enabled GST debugging (GST_DEBUG=3,gst-play*:7)

Fixes: #455

The associated callbacks should capture weak references of required
objects with which they are will interact otherwise it leads
to leaked underlying gst resources on high level owner (Player)
destruction due to the cyclic reference dependency (between
PlayerInner, ServoSrc and the associated callbacks).

PlayerInner -> PlaySignalAdapter callbacks (strong -> weak refs)
ServoSrc -> ServoSrc callbacks (strong -> weak refs)

Testing: Manually testing with the following WPT test
./mach test-wpt html/semantics/embedded-content/the-video-element/intrinsic_sizes.htm -r
with enabled GST debugging (GST_DEBUG=3,gst-play*:7)

Fixes: servo#455

Signed-off-by: Andrei Volykhin <[email protected]>
@tharkum
Copy link
Contributor Author

tharkum commented Oct 29, 2025

@jdm < Please take a look

@tharkum
Copy link
Contributor Author

tharkum commented Oct 29, 2025

There is crash under heavy testing scenario, so please postpone review

 0:30.53 pid:2649784    0: servoshell::backtrace::print
 0:30.53 pid:2649784    1: servoshell::crash_handler::install::handler
 0:30.57 pid:2649784    2: <unknown>
 0:30.57 pid:2649784    3: mozalloc_abort
 0:30.57 pid:2649784    4: abort
 0:30.57 pid:2649784    5: g_mutex_clear
 0:30.57 pid:2649784    6: <unknown>
 0:30.58 pid:2649784    7: g_object_unref
 0:30.58 pid:2649784    8: g_object_get
 0:30.58 pid:2649784    9: gst_play_get_position
 0:30.58 pid:2649784   10: <unknown>
 0:30.58 pid:2649784   11: g_cclosure_marshal_VOID__BOXEDv
 0:30.58 pid:2649784   12: g_signal_emit_valist
 0:30.58 pid:2649784   13: g_signal_emit
 0:30.58 pid:2649784   14: gst_bus_async_signal_func
 0:30.58 pid:2649784   15: <unknown>
 0:30.58 pid:2649784   16: g_main_context_dispatch
 0:30.58 pid:2649784   17: <unknown>
 0:30.58 pid:2649784   18: g_main_loop_run
 0:30.58 pid:2649784   19: <unknown>
 0:30.58 pid:2649784   20: <unknown>
 0:30.62 pid:2649784   21: start_thread
 0:30.62 pid:2649784              at ./nptl/pthread_create.c:442:8
 0:30.62 pid:2649784   22: __GI___clone3
 0:30.62 pid:2649784              at ./misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:81:0

@sdroege
Copy link
Contributor

sdroege commented Oct 30, 2025

PlayerInner -> PlaySignalAdapter callbacks (strong -> weak refs)

Instead of going this way, you might want to use the gst::Bus on the Player instance directly instead of using the signals via callbacks. That makes it (in my experience) simpler to decouple ownership of values and avoid circular references.

Comment on lines +591 to +602
signal_adapter.connect_media_info_updated({
let observer = self.observer.clone();
let weak_inner = weak_inner.clone();

move |_, info| {
let Ok(metadata) = metadata_from_media_info(info) else {
return;
};

let Some(strong_inner) = weak_inner.upgrade() else {
return;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
signal_adapter.connect_media_info_updated({
let observer = self.observer.clone();
let weak_inner = weak_inner.clone();
move |_, info| {
let Ok(metadata) = metadata_from_media_info(info) else {
return;
};
let Some(strong_inner) = weak_inner.upgrade() else {
return;
};
signal_adapter.connect_media_info_updated(glib::clone!(
#[strong(rename_to = observer]
self.observer,
#[weak]
inner,
move |_, info| {
let Ok(metadata) = metadata_from_media_info(info) else {
return;
};

would have the same effect btw, if you want to avoid a bit of boilerplate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow. Thanks. I will look into glib:clone macr, but it also looks like a boilerplate...

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, it exists to reduce this kind of boilerplate :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sdroege < If I am understanding correctly glib::clone macro will upgrade weak to strong at the beginning of the passing closure execution (no way to delay this right before variable usage)?

https://docs.rs/glib-macros/0.21.0/src/glib_macros/clone.rs.html#544

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct. Do you need to do it later?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are a risk that closure will be the latest owner of the object and this object will be destroyed on the wrong thread (like it happens in #456 (comment) but with higher chance of failure).

Anyway I will try to fix mentioned crash and return back to this ....

Copy link
Contributor

Choose a reason for hiding this comment

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

There should be no "wrong thread" though. That seems like a deeper problem that should be fixed.

Comment on lines +702 to +715
inner.player.pipeline().connect("source-setup", false, {
let is_ready = self.is_ready.clone();
let observer = self.observer.clone();
let sender = sender.clone();
let weak_inner = weak_inner.clone();

move |args| {
let source = args[1].get::<gst::Element>().unwrap();

let Some(strong_inner) = weak_inner.upgrade() else {
return None;
};

let mut inner = strong_inner.lock().unwrap();
Copy link
Contributor

@sdroege sdroege Nov 1, 2025

Choose a reason for hiding this comment

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

Also here:

Suggested change
inner.player.pipeline().connect("source-setup", false, {
let is_ready = self.is_ready.clone();
let observer = self.observer.clone();
let sender = sender.clone();
let weak_inner = weak_inner.clone();
move |args| {
let source = args[1].get::<gst::Element>().unwrap();
let Some(strong_inner) = weak_inner.upgrade() else {
return None;
};
let mut inner = strong_inner.lock().unwrap();
inner.player.pipeline().connect_closure("source-setup", false, glib::closure!(
#[strong(rename_to = is_ready)]
self.is_ready,
#[strong(rename_to = observer)]
self.observer,
#[strong]
sender,
#[weak]
inner,
move |_pipeline: &gst::Element, source: &gst::Element| {
let mut inner = inner.lock().unwrap();

Copy link
Contributor

Choose a reason for hiding this comment

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

(in addition to what glib::clone! can do for closures/async blocks, glib::closure! also unpacks the arguments from a &[glib::Value] to the actual arguments for you)

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.

GStreamer: Cyclic reference dependency between the objects and associated callbacks

2 participants