Skip to content

Commit dc0685e

Browse files
committed
Add support for ChromeDriver, so we can configure mobile emulation
1 parent 075932b commit dc0685e

File tree

10 files changed

+796
-51
lines changed

10 files changed

+796
-51
lines changed

Cargo.lock

Lines changed: 535 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ serde_with = "3.9.0"
1818
toml = "0.8.19"
1919
tracing = "0.1.40"
2020
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
21+
webdriver_client = { version = "0.2.5", git = "https://github.com/delan/webdriver_client_rust.git", branch = "bump-log-to-0.4" }
2122

2223
[profile.release]
2324
debug = true

chromedriver.nix

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{ lib
2+
, stdenv
3+
, fetchurl
4+
, unzip
5+
, autoPatchelfHook
6+
, glib
7+
, nss
8+
, xorg
9+
}:
10+
11+
stdenv.mkDerivation rec {
12+
pname = "chromedriver";
13+
version = "130.0.6723.91";
14+
15+
src = let
16+
json = builtins.fromJSON (builtins.readFile (fetchurl {
17+
url = "https://googlechromelabs.github.io/chrome-for-testing/${version}.json";
18+
hash = "sha256-LS6n73mzL46AQtv7FvCRguGf090NyaPvotKxUueOIj0=";
19+
}));
20+
url = (lib.findFirst (d: d.platform == "linux64") null json.downloads.chromedriver).url;
21+
in fetchurl {
22+
url = url;
23+
hash = "sha256-qMlM6ilsIqm8G5KLE4uGVb/s2bNyZSyQmxsq+EHKX/c=";
24+
};
25+
26+
nativeBuildInputs = [
27+
unzip
28+
autoPatchelfHook
29+
];
30+
31+
buildInputs = [
32+
glib
33+
nss
34+
xorg.libxcb
35+
];
36+
37+
sourceRoot = ".";
38+
39+
installPhase = ''
40+
install -m755 -D chromedriver-linux64/chromedriver $out/bin/chromedriver
41+
'';
42+
}

query-path.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh
2+
command -v "$1"

shell.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{ pkgs ? import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/dc460ec76cbff0e66e269457d7b728432263166c.tar.gz"; }) {} }:
2+
pkgs.mkShell {
3+
buildInputs = [
4+
(pkgs.callPackage ./chromedriver.nix {})
5+
];
6+
}

src/analyse.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fn analyse_sample(
5454
}
5555
}
5656
}
57-
Engine::Chromium { .. } => {
57+
Engine::Chromium { .. } | Engine::ChromeDriver { .. } => {
5858
let mut json_paths = vec![];
5959
let mut convert_jobs = vec![];
6060
for entry in std::fs::read_dir(&sample_dir)? {
@@ -99,7 +99,9 @@ fn analyse_sample(
9999

100100
let summaries = match engine.engine {
101101
Engine::Servo { .. } => crate::servo::compute_summaries(args)?,
102-
Engine::Chromium { .. } => crate::chromium::compute_summaries(args)?,
102+
Engine::Chromium { .. } | Engine::ChromeDriver { .. } => {
103+
crate::chromium::compute_summaries(args)?
104+
}
103105
};
104106

105107
File::create(sample_dir.join("summaries.json"))?.write_all(summaries.json().as_bytes())?;

src/collect.rs

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
use std::{fs::create_dir_all, path::Path, process::Command};
1+
use core::str;
2+
use std::{
3+
collections::BTreeMap,
4+
fs::{copy, create_dir_all, read_dir, File},
5+
path::Path,
6+
process::Command,
7+
thread::sleep,
8+
};
29

310
use jane_eyre::eyre::{self, bail, eyre, OptionExt};
4-
use tracing::info;
11+
use serde_json::json;
12+
use tracing::{debug, info};
13+
use webdriver_client::{chrome::ChromeDriver, messages::NewSessionCmd, Driver, LocationStrategy};
514

615
use crate::{
716
shell::SHELL,
8-
study::{KeyedCpuConfig, KeyedEngine, KeyedSite, Study},
17+
study::{Engine, KeyedCpuConfig, KeyedEngine, KeyedSite, Study},
918
};
1019

1120
pub fn main(args: Vec<String>) -> eyre::Result<()> {
@@ -60,6 +69,132 @@ fn create_sample(
6069
return Ok(());
6170
}
6271

72+
if let Engine::ChromeDriver { path } = engine.engine {
73+
// Resolve path against PATH if needed. ChromeDriver or WebDriver seems to need this.
74+
let query = SHELL
75+
.lock()
76+
.map_err(|e| eyre!("Mutex poisoned: {e:?}"))?
77+
.run(include_str!("../query-path.sh"), [path])?
78+
.output()?;
79+
if !query.status.success() {
80+
bail!("Process failed: {}", query.status);
81+
}
82+
let path = str::from_utf8(&query.stdout)?
83+
.strip_suffix("\n")
84+
.ok_or_eyre("Output has no trailing newline")?;
85+
86+
for i in 1..=study.sample_size {
87+
info!("Starting ChromeDriver");
88+
let driver =
89+
ChromeDriver::spawn().map_err(|e| eyre!("Failed to spawn ChromeDriver: {e}"))?;
90+
91+
// Configure the browser with WebDriver capabilities. Note that ChromeDriver takes care
92+
// of running Chromium with a clean profile (much like `--user-data-dir=$(mktemp -d)`)
93+
// and in a way amenable to automation (e.g. `--no-first-run`).
94+
// <https://www.w3.org/TR/webdriver/#capabilities>
95+
// <https://developer.chrome.com/docs/chromedriver/capabilities>
96+
let mut params = NewSessionCmd::default();
97+
// Do not wait for page load to complete.
98+
params.always_match("pageLoadStrategy", json!("none"));
99+
// Allow the use of mitmproxy replay (see ../start-mitmproxy.sh).
100+
params.always_match("acceptInsecureCerts", json!(true));
101+
102+
let mut mobile_emulation = BTreeMap::default();
103+
if let Some(user_agent) = site.user_agent {
104+
// ChromeDriver does not support the standard `userAgent` capability, which goes in
105+
// the top level. Use `.goog:chromeOptions.mobileEmulation.userAgent` instead.
106+
mobile_emulation.insert("userAgent", json!(user_agent));
107+
}
108+
if let Some((width, height)) = site.screen_size()? {
109+
mobile_emulation
110+
.insert("deviceMetrics", json!({ "width": width, "height": height }));
111+
}
112+
113+
let pftrace_temp_dir = mktemp::Temp::new_dir()?;
114+
let attempted_pftrace_temp_path = pftrace_temp_dir.join("chrome.pftrace");
115+
let attempted_pftrace_temp_path = attempted_pftrace_temp_path
116+
.to_str()
117+
.ok_or_eyre("Unsupported path")?;
118+
let mut args = vec![
119+
"--trace-startup".to_owned(),
120+
format!("--trace-startup-file={attempted_pftrace_temp_path}"),
121+
];
122+
args.extend(site.extra_engine_arguments(engine.key).to_owned());
123+
params.always_match(
124+
"goog:chromeOptions",
125+
json!({
126+
// <https://developer.chrome.com/docs/chromedriver/capabilities>
127+
"mobileEmulation": mobile_emulation,
128+
"binary": path,
129+
"args": args,
130+
}),
131+
);
132+
133+
info!("Starting Chromium");
134+
let session = driver.session(&params)?;
135+
136+
info!(site.url, "Navigating to site");
137+
session.go(site.url)?;
138+
139+
info!(?site.browser_open_time, "Waiting for fixed amount of time");
140+
sleep(site.browser_open_time);
141+
142+
info!(wait_for_selectors = ?site.wait_for_selectors().collect::<Vec<_>>(), "Checking for elements");
143+
#[derive(Debug)]
144+
struct ElementCounts {
145+
expected: usize,
146+
actual: usize,
147+
}
148+
let element_counts = site
149+
.wait_for_selectors()
150+
.map(
151+
|(selector, expected)| -> eyre::Result<(&String, ElementCounts)> {
152+
Ok((
153+
selector,
154+
ElementCounts {
155+
expected: *expected,
156+
actual: session
157+
.find_elements(selector, LocationStrategy::Css)?
158+
.len(),
159+
},
160+
))
161+
},
162+
)
163+
.collect::<eyre::Result<BTreeMap<&String, ElementCounts>>>()?;
164+
debug!(?element_counts, "Found elements");
165+
for (selector, ElementCounts { expected, actual }) in element_counts {
166+
assert_eq!(expected, actual, "Condition failed: wait_for_selectors.{selector:?}: expected {expected}, actual {actual}");
167+
}
168+
169+
// When using ChromeDriver, for some reason, Chromium fails to rename the Perfetto trace
170+
// to `--trace-startup-file`. Kill ChromeDriver and rename it ourselves.
171+
drop(session);
172+
let pftrace_path = sample_dir.join(format!(
173+
"chrome{:0width$}.pftrace",
174+
i,
175+
width = study.sample_size.to_string().len()
176+
));
177+
let pftrace_path = pftrace_path.to_str().ok_or_eyre("Unsupported path")?;
178+
for entry in read_dir(&pftrace_temp_dir)? {
179+
let pftrace_temp_path = entry?.path();
180+
info!(
181+
?pftrace_temp_path,
182+
?pftrace_path,
183+
"Copying Perfetto trace to sample directory"
184+
);
185+
copy(pftrace_temp_path, pftrace_path)?;
186+
}
187+
188+
// Extend the lifetime of `pftrace_temp_dir` to avoid premature deletion.
189+
drop(pftrace_temp_dir);
190+
}
191+
192+
info!("Marking sample as done");
193+
File::create_new(sample_dir.join("done"))?;
194+
195+
return Ok(());
196+
}
197+
63198
let sample_dir = sample_dir.to_str().ok_or_eyre("Bad sample path")?;
64199
info!("Creating sample");
65200
let mut args = vec![

src/report.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@ pub fn main(args: Vec<String>) -> eyre::Result<()> {
6565
// If there were any Chromium results, print sections for real Chromium events.
6666
if study
6767
.engines()
68-
.find(|engine| matches!(engine.engine, Engine::Chromium { .. }))
68+
.find(|engine| {
69+
matches!(
70+
engine.engine,
71+
Engine::Chromium { .. } | Engine::ChromeDriver { .. },
72+
)
73+
})
6974
.is_some()
7075
{
7176
for summary_key in REAL_CHROMIUM_EVENTS.split(" ") {

src/study.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{collections::BTreeMap, fs::File, io::Read, path::Path, time::Duration};
22

3-
use jane_eyre::eyre;
3+
use jane_eyre::eyre::{self, bail};
44
use serde::Deserialize;
55

66
#[derive(Debug, Deserialize)]
@@ -29,6 +29,9 @@ enum Site {
2929
Full {
3030
url: String,
3131
browser_open_time: Option<u64>,
32+
user_agent: Option<String>,
33+
screen_size: Option<Vec<usize>>,
34+
wait_for_selectors: Option<BTreeMap<String, usize>>,
3235
extra_engine_arguments: Option<BTreeMap<String, Vec<String>>>,
3336
},
3437
}
@@ -37,6 +40,9 @@ pub struct KeyedSite<'study> {
3740
pub key: &'study str,
3841
pub url: &'study str,
3942
pub browser_open_time: Duration,
43+
pub user_agent: Option<&'study str>,
44+
screen_size: Option<&'study [usize]>,
45+
wait_for_selectors: Option<&'study BTreeMap<String, usize>>,
4046
extra_engine_arguments: Option<&'study BTreeMap<String, Vec<String>>>,
4147
}
4248

@@ -45,6 +51,7 @@ pub struct KeyedSite<'study> {
4551
pub enum Engine {
4652
Servo { path: String },
4753
Chromium { path: String },
54+
ChromeDriver { path: String },
4855
}
4956
#[derive(Clone, Copy, Debug)]
5057
pub struct KeyedEngine<'study> {
@@ -90,24 +97,52 @@ impl<'study> From<(&'study str, &'study Site)> for KeyedSite<'study> {
9097
key,
9198
url,
9299
browser_open_time: default_browser_open_time,
100+
user_agent: None,
101+
screen_size: None,
102+
wait_for_selectors: None,
93103
extra_engine_arguments: None,
94104
},
95105
Site::Full {
96106
url,
97-
extra_engine_arguments,
98107
browser_open_time,
108+
user_agent,
109+
screen_size,
110+
wait_for_selectors,
111+
extra_engine_arguments,
99112
} => Self {
100113
key,
101114
url,
102115
browser_open_time: browser_open_time
103116
.map_or(default_browser_open_time, Duration::from_secs),
117+
user_agent: user_agent.as_deref(),
118+
screen_size: screen_size.as_deref(),
119+
wait_for_selectors: wait_for_selectors.as_ref(),
104120
extra_engine_arguments: extra_engine_arguments.as_ref(),
105121
},
106122
}
107123
}
108124
}
109125

110126
impl KeyedSite<'_> {
127+
pub fn screen_size(&self) -> eyre::Result<Option<(usize, usize)>> {
128+
self.screen_size
129+
.map(|size| {
130+
Ok(match size {
131+
[width, height] => (*width, *height),
132+
other => bail!("Bad screen_size: {other:?}"),
133+
})
134+
})
135+
.transpose()
136+
}
137+
138+
pub fn wait_for_selectors(&self) -> Box<dyn Iterator<Item = (&String, &usize)> + '_> {
139+
if let Some(wait_for_selectors) = self.wait_for_selectors {
140+
Box::new(wait_for_selectors.iter())
141+
} else {
142+
Box::new([].into_iter())
143+
}
144+
}
145+
111146
pub fn extra_engine_arguments(&self, engine_key: &str) -> &[String] {
112147
self.extra_engine_arguments
113148
.and_then(|map| map.get(engine_key))
@@ -120,13 +155,17 @@ impl KeyedEngine<'_> {
120155
match self.engine {
121156
Engine::Servo { .. } => include_str!("../benchmark-servo.sh"),
122157
Engine::Chromium { .. } => include_str!("../benchmark-chromium.sh"),
158+
Engine::ChromeDriver { .. } => {
159+
panic!("BUG: Engine::ChromeDriver has no benchmark runner script")
160+
}
123161
}
124162
}
125163

126164
pub fn browser_path(&self) -> &str {
127165
match self.engine {
128166
Engine::Servo { path } => path,
129167
Engine::Chromium { path } => path,
168+
Engine::ChromeDriver { path } => path,
130169
}
131170
}
132171
}

studies/example/study.toml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,38 @@ isolate_cpu_command = ["sudo", "../../isolate-cpu-for-shell.sh"] # on Linux
2727
# Sites can also have other settings, in the full table format.
2828
# - `url` has the same meaning as the string value above
2929
# - `browser_open_time` (optional) is in seconds
30+
# - `user_agent` (optional) overrides the browser’s default user agent
31+
# - Currently supported for `ChromeDriver`-type engines only
32+
# - For `Servo`-type engines, use `extra_engine_arguments.engine = ["--user-agent", "Android"]`
33+
# - For `Chromium`-type engines, use `extra_engine_arguments.engine = ["--user-agent=Android"]`
34+
# - `screen_size` (optional) overrides the browser’s reported screen size (not the viewport size!)
35+
# - Currently supported for `ChromeDriver`-type engines only
36+
# - For `Servo`-type engines, use `extra_engine_arguments.engine = ["--screen-size", "320x568"]`
37+
# - For `Chromium`-type engines, there is no way to do this
38+
# - `wait_for_selectors` (optional) is a map from CSS selectors to expected element counts
39+
# - Currently supported for `ChromeDriver`-type engines only
40+
# - For `Servo`-type engines, there is no way to do this
41+
# - For `Chromium`-type engines, there is no way to do this
3042
# - `extra_engine_arguments` (optional) is keyed on the engine key
3143
# [sites."example.com"]
3244
# url = "http://example.com/"
3345
# browser_open_time = 20
46+
# user_agent = "Android"
47+
# screen_size = [320,568]
48+
# wait_for_selectors."nav a" = 3
3449
# extra_engine_arguments.servo1 = ["--pref", "dom.svg.enabled"]
3550
# extra_engine_arguments.servo2 = ["--pref", "dom.svg.enabled"]
3651

3752
# Define your engines here.
3853
# - Syntax is `key = { type = "Servo|Chromium", path = "/path/to/browser" }`
3954
# - Dots in the key must be quoted
55+
# - `type` is one of the following:
56+
# - `Servo` uses benchmark-servo.sh
57+
# - `Chromium` uses benchmark-chromium.sh
58+
# - `ChromeDriver` uses ChromeDriver, a WebDriver-based approach
4059
# - If `path` has no slashes, it represents a command in your PATH
4160
[engines]
4261
"servo1" = { type = "Servo", path = "/path/to/servo1/servo" }
4362
"servo2" = { type = "Servo", path = "/path/to/servo2/servo" }
44-
"chromium" = { type = "Chromium", path = "google-chrome-stable" }
63+
"chromium1" = { type = "Chromium", path = "google-chrome-stable" }
64+
"chromium2" = { type = "ChromeDriver", path = "google-chrome-stable" }

0 commit comments

Comments
 (0)