对于 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 中的一切正常工作。