Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion marketplace/official-plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@
{
"id": "servo",
"name": "Servo Web Renderer",
"version": "0.2.4",
"version": "0.2.5",
"node_kind": "servo",
"kind": "native",
"entrypoint": "libservo_web.so",
Expand Down
2 changes: 1 addition & 1 deletion plugins/native/servo/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions plugins/native/servo/Cargo.toml
Comment thread
staging-devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[package]
name = "servo-plugin-native"
version = "0.2.4"
version = "0.2.5"
edition = "2021"
license = "MPL-2.0"

Expand Down Expand Up @@ -42,10 +42,10 @@ tracing = "0.1"
tikv-jemalloc-sys = { version = "0.6", features = ["disable_initial_exec_tls"] }

[dev-dependencies]
# The cross-session leak integration test drives the real Servo engine,
# which has no clean global-shutdown path: at process exit mozjs' C++
# destructors race the still-live renderer thread and SIGSEGV. The test
# calls `libc::_exit` after asserting to bypass that teardown.
# The integration tests drive the real Servo engine, which has no clean
# global-shutdown path: at process exit mozjs' C++ destructors race the
# still-live renderer thread and SIGSEGV. Each test calls `libc::_exit`
# after asserting to bypass that teardown.
libc = "0.2"

[lints.clippy]
Expand Down
2 changes: 1 addition & 1 deletion plugins/native/servo/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

id: servo
name: Servo Web Renderer
version: 0.2.4
version: 0.2.5
node_kind: servo
kind: native
entrypoint: libservo_web.so
Expand Down
4 changes: 4 additions & 0 deletions plugins/native/servo/src/servo_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ fn handle_register(
let mut prefs = servo::Preferences {
network_http_proxy_uri: String::new(),
network_https_proxy_uri: String::new(),
// WebGL2 is off by default in Servo; without it pages that
// require a webgl2 context (getContext returns null) render
// as static content with no animation.
dom_webgl2_enabled: true,
Comment on lines +599 to +602

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Preference is only applied when the first servo node constructs the engine

dom_webgl2_enabled is set inside servo.get_or_insert_with(...), so like the User-Agent it is fixed by whichever node creates the process-global Servo first. That is fine for a hardcoded constant (every node gets the same value), but it means the flag cannot be made per-node/configurable later without restructuring engine construction. Worth noting in case a enable_webgl2 config knob is desired.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right — like the User-Agent, it's fixed by whichever node constructs the process-global Servo first. Since it's a hardcoded constant every node gets the same value, so this is fine; a per-node enable_webgl2 knob would need engine-construction restructuring (same bucket as the per-instance isolation work tracked in #639/#666).

..servo::Preferences::default()
};
// `Preferences` is a process-global singleton, so the User-Agent of
Expand Down
129 changes: 129 additions & 0 deletions plugins/native/servo/tests/animation_repro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: © 2025 StreamKit Contributors
//
// SPDX-License-Identifier: MPL-2.0

//! Regression test: animated pages must produce changing frames.
//!
//! A `data:` page drives a `requestAnimationFrame` loop that clears a
//! WebGL2 canvas with a colour that changes every frame. Successive
//! rendered frames must differ; a frozen animation (e.g. WebGL2 disabled,
//! so `getContext('webgl2')` returns null and the page's render loop
//! never starts) fails the test.
//!
//! This lives in its own integration binary because it boots the real
//! Servo engine (software-rendered via llvmpipe — no GPU/display needed)
//! and is far too heavy for the workspace `cargo test`.

use std::ffi::c_char;
use std::io::Write;
use std::os::raw::c_void;
use std::sync::mpsc::Receiver;
use std::time::Duration;

use servo_web::test_api::{
send_work, CLogLevel, Logger, NodeId, ServoConfig, ServoThreadResult, ServoWorkItem,
};

const extern "C" fn noop_log(
_level: CLogLevel,
_target: *const c_char,
_message: *const c_char,
_user_data: *mut c_void,
) {
}

const DIM: u32 = 64;
/// Generous tick budget so the page reaches first paint and animates even
/// on a slow/loaded CI box.
const MAX_TICKS: usize = 600;
/// Frames that differ from their predecessor before declaring success.
const REQUIRED_CHANGES: usize = 10;

fn send(item: ServoWorkItem) {
if let Err(e) = send_work(item) {
panic!("send_work failed: {e}");
}
}

fn register(url: &str) -> (NodeId, Receiver<ServoThreadResult>) {
let node_id = uuid::Uuid::new_v4();
let (tx, rx) = std::sync::mpsc::sync_channel(2);
let config =
ServoConfig { url: url.to_string(), width: DIM, height: DIM, ..ServoConfig::default() };
let logger = Logger::new(noop_log, std::ptr::null_mut(), "servo-test");
send(ServoWorkItem::Register { node_id, config, result_tx: tx, logger });
match rx.recv() {
Ok(ServoThreadResult::InitOk) => {},
Ok(ServoThreadResult::InitErr(e)) => panic!("init failed: {e}"),
Ok(_) => panic!("unexpected result during init"),
Err(e) => panic!("init recv failed: {e}"),
}
(node_id, rx)
}

fn render(node_id: NodeId, rx: &Receiver<ServoThreadResult>) -> Vec<u8> {
send(ServoWorkItem::Render { node_id });
match rx.recv() {
Ok(ServoThreadResult::Frame { rgba_data, .. }) => rgba_data,
Ok(_) => panic!("expected frame result"),
Err(e) => panic!("frame recv failed: {e}"),
}
}

#[test]
fn animated_webgl2_page_frames_change() {
// No webgl1 fallback: the test must fail if WebGL2 is unavailable,
// since real pages (e.g. webgl2fundamentals.org's background) bail out
// of their animation loop entirely when getContext('webgl2') is null.
let url = "data:text/html,<body style='margin:0'>\
<canvas id=c width=64 height=64></canvas>\
<script>\
const gl=document.getElementById('c').getContext('webgl2');\
if(gl){let n=0;function f(){n=(n+0.03)%1;\
gl.clearColor(n,1-n,0.5,1);gl.clear(gl.COLOR_BUFFER_BIT);\
requestAnimationFrame(f)}requestAnimationFrame(f)}\
</script></body>";

let (id, rx) = register(url);

let mut last: Option<Vec<u8>> = None;
let mut changes = 0usize;
let mut painted_frames = 0usize;
for _ in 0..MAX_TICKS {
let frame = render(id, &rx);
// Pre-paint frames are fully transparent; only compare painted ones.
if frame.chunks_exact(4).any(|px| px[3] >= 128) {
painted_frames += 1;
if last.as_deref().is_some_and(|prev| prev != frame) {
changes += 1;
}
last = Some(frame);
}
if changes >= REQUIRED_CHANGES {
break;
}
std::thread::sleep(Duration::from_millis(33));
}
Comment on lines +92 to +106

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔍 Test may be flaky if WebGL2 canvas contents are not preserved across readback

The regression test relies on the WebGL2 drawing buffer's cleared colour reaching the composited surface. The canvas is created without preserveDrawingBuffer, and the readback happens asynchronously via handle_render on a separate tick from the rAF callback. If Servo's software WebGL2 path ever composites a stale/blank buffer, painted_frames could stay 0 and the test would fail for reasons unrelated to the pref. Consider drawing a DOM/CSS-independent fallback marker or asserting with a longer budget if CI flakes are observed.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Noted. In practice Servo's software WebGL path composites the cleared buffer reliably — the test converges in ~12 painted frames locally, well under the 600-tick budget, and even if the canvas ever composited blank the page background itself still paints (so painted_frames wouldn't stay 0; only changes would stall, which is exactly the regression signal we want). If CI ever flakes here we can add a DOM-side fallback marker, but I'd rather not weaken the WebGL2 assertion preemptively.

Comment on lines +92 to +106

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Test can pass with a static page if frames differ for other reasons

The frame-change detection compares full RGBA buffers, so any nondeterministic pixel noise (antialiasing, scrollbar/caret blink, compositor artifacts) would also count as a "change" and let the test pass even with WebGL2 disabled. Conversely with a 33 ms sleep and 600 ticks, the ~20 s worst-case runtime plus real-Servo boot makes this a heavy test to run in CI; the doc comment acknowledges it is not part of the workspace cargo test.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

On false-passes: with WebGL2 disabled I measured 0–2 changed frames over 600 ticks on this exact setup (the software renderer is deterministic — no AA noise, no caret/scrollbar on this page), so REQUIRED_CHANGES = 10 has comfortable margin. On runtime: the loop breaks as soon as 10 changes are seen, which is ~1–2 s after first paint in practice; 600 ticks is only the worst-case budget for a slow box before failing.


send(ServoWorkItem::Unregister { node_id: id });

println!("painted_frames = {painted_frames}, changed_frames = {changes}");
assert!(painted_frames > 0, "page never painted");
assert!(
changes >= REQUIRED_CHANGES,
"animation is frozen: only {changes} of {painted_frames} painted frames changed"
);

// Servo's embedded engine has no clean global-shutdown path: at normal
// process exit, mozjs' C++ static destructors race the still-live
// renderer thread and abort with a SIGSEGV, which would fail this
// otherwise-passing test. This is the only test in this binary, so
// exiting here — after all assertions have held — is safe and bypasses
// that teardown entirely.
println!("animated_webgl2_page_frames_change: PASSED");
let _ = std::io::stdout().flush();
// SAFETY: `_exit` is async-signal-safe and simply terminates the
// process with status 0 without running atexit handlers or C++ static
// destructors (the source of the teardown SIGSEGV above).
unsafe { libc::_exit(0) };
Comment on lines +117 to +128

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Regression test terminates the whole test process on success

animation_repro.rs ends with libc::_exit(0) after its assertions. This is safe only because the binary contains a single test; adding any second #[test] to this file would silently skip it (or cause nondeterministic skipping under the default multi-threaded harness) since the process is killed as soon as this one passes. The comment documents the constraint, but it is an easy trap for future additions.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — this follows the same single-test-per-binary convention as cross_session_leak.rs (which has the same libc::_exit(0) teardown workaround), and the doc comment states the constraint. Any future Servo integration test should go in its own binary for the same reason.

}
Loading