对于 D-Installer,我们已经有一个 Ruby CLI,它是作为概念验证而创建的。然后作为黑客周的一部分,我们创建了另一个 Rust 版本,以便学习一些 Rust 并亲自动手。现在我们已经熟悉了两者,我们想测量在 Rust 和 Ruby 中调用 D-Bus 方法的开销,以确保如果我们继续使用 Rust,我们不会对其速度感到惊讶。(提示:我们不期望出现这种情况,但期望和事实可能不同)。
小型 CLI 场景
由于我们主要想测量开销,因此我们使用一个简单的程序,从 d-bus 读取一个属性并将其打印到 stdout。该属性的数据结构并非微不足道,因此也测试了数据编组的效率。我们使用 D-Installer 中拥有的 D-Bus 接口,该属性是可用基本产品的列表。
用于与 D-Bus 通信的库是众所周知的。对于 D-Bus,我们使用 rubygem-dbus,对于 rust,我们使用 zbus。为了保持代码简单,我们不使用库中的高级功能,例如创建对象/代理,而是使用简单的直接调用。
Ruby 代码
require "dbus"
sysbus = DBus.system_bus
service = sysbus["org.opensuse.DInstaller.Software"]
object = service["/org/opensuse/DInstaller/Software1"]
interface = object["org.opensuse.DInstaller.Software1"]
products = interface["AvailableBaseProducts"]
puts "output: #{products.inspect}"
Rust 代码
use zbus::blocking::{Connection, Proxy};
fn main() {
let connection = Connection::system().unwrap();
let proxy = Proxy::new(&connection,
"org.opensuse.DInstaller.Software",
"/org/opensuse/DInstaller/Software1",
"org.opensuse.DInstaller.Software1").unwrap();
let res: Vec<(String,String)> = proxy.get_property("AvailableBaseProducts").unwrap();
println!("output: {:?}", res);
return;
}
结果
为了获得一些合理的数据,我们运行它一百次并使用 time 实用程序进行测量。
这是 ruby 3.1 的结果
time for i in {1..100}; do ruby dbus_measure.rb &> /dev/null; done
real 0m40.491s
user 0m18.599s
sys 0m3.823s
这是 ruby 3.2 的结果
time for i in {1..100}; do ruby dbus_measure.rb &> /dev/null; done
real 0m31.025s
user 0m16.412s
sys 0m3.441s
为了比较,rust 使用 --release 构建
time for i in {1..100}; do ./dbus_measure &> /dev/null; done
real 0m10.286s
user 0m0.254s
sys 0m0.188s
如你所见,rust 看起来快得多。也很高兴看到在 Ruby3.2 中,冷启动得到了很好的改进。我们还与 ruby-dbus 维护者讨论了这个问题,他提到 ruby dbus 在对象上调用内省,并且有 一种避免这种情况的方法
总体印象是,如果你想要一个需要调用 d-bus 的小型 CLI 实用程序,那么 rust 更适合它。
多次调用场景
我们的 CLI 在某些情况下只是一个 dbus 调用,例如当你设置一些 DInstaller 选项时,但也有其他情况,例如一个长时间运行的探测,需要进度报告,在这种情况下会有更多的 dbus 调用。所以我们想模拟需要多次调用进度的情况。
Ruby 代码
require "dbus"
sysbus = DBus.system_bus
service = sysbus["org.opensuse.DInstaller.Software"]
object = service["/org/opensuse/DInstaller/Software1"]
interface = object["org.opensuse.DInstaller.Software1"]
100.times do
products = interface["AvailableBaseProducts"]
end
Rust 代码
use zbus::blocking::{Connection, Proxy};
fn main() {
let connection = Connection::system().unwrap();
let proxy = Proxy::new(&connection,
"org.opensuse.DInstaller.Software",
"/org/opensuse/DInstaller/Software1",
"org.opensuse.DInstaller.Software1").unwrap();
for _ in 1..100 {
let _: Vec<(String,String)> = proxy.get_property("AvailableBaseProducts").unwrap();
}
return;
}
结果
我们没有看到不同 Ruby 版本之间的差异,所以我们只显示时间
time ruby dbus_measure.rb
real 0m10.529s
user 0m0.372s
sys 0m0.039s
time ./dbus_measure
real 0m0.052s
user 0m0.005s
sys 0m0.003s
这里变得更加有趣,原因揭示了 busctl --system monitor org.opensuse.DInstaller.Software。Rust 在其代理中缓存属性,并且只进行一次 dbus 调用 GetAll 来初始化所有属性。另一方面,ruby 库首先调用内省,然后调用 Get 并指定属性。ruby 也可以实现相同的行为,但需要更多的工作。因此,即使对于需要多次调用 D-Bus 的简单 CLI,rust 看起来也足够快。我们剩下的唯一问题是,rust 代理是否能正确检测到属性何时更改(换句话说,默认情况下它发送观察者信号时)。
出于这个原因,我们创建了一个带有 sleep 的简单 rust 程序,并使用 d-feet 来更改属性。
use zbus::blocking::{Connection, Proxy};
use std::{thread, time};
fn main() {
let connection = Connection::system().unwrap();
let proxy = Proxy::new(&connection,
"org.opensuse.DInstaller.Software",
"/org/opensuse/DInstaller/Software1",
"org.opensuse.DInstaller.Software1").unwrap();
let second = time::Duration::from_secs(1);
for _ in 1..100 {
let res: String = proxy.get_property("SelectedBaseProduct").unwrap();
println!("output {}", res);
thread::sleep(second)
}
return;
}
我们已成功验证 rust 中的一切正常工作。