用Rust写WebAssembly,解析RRule快了5倍

手撕RRule是什么体验?一个字,难;四个字,我太难了~

前言

为什么会产生自己写一个rrule的想法呢?现成的库,它不香么?

这得从一个巧合说起:某一天,我听说WebAssembly很快,又一天,我听说RustWebAssembly很快,再一天,我发现我们日历项目里,RRule解析重复规则很慢。于是一拍手,要不用rust写一个rrule然后打包成WebASsembly吧~。然后苦逼的日子就开始了。

项目已经发布到Npm@suilang/rrule-rust,基础的解析能力已经实现。后续会陆续补齐如生成字符串、设置排除时间等能力,有兴趣的同学可以看下。

标准场景下,执行效率会比rrule.js快4到5倍,如果是加上时区,会快100倍左右。(最新的rrule.js对于时区的计算有些性能问题,不知道什么时候会修)。

RRule是什么

rrule是一个用于解析日历循环的工具,它遵循了 iCalendar 规范(RFC 5545)。iCalendar 是一种通用的日历数据交换格式,用于描述日历事件和重复规则。

rrule的主要用途是根据指定的重复规则生成符合规则的日期序列。它可以处理各种复杂的重复规则,例如每天、每周、每月、每年的重复,以及每隔一定时间间隔重复的规则。它还支持排除特定日期、指定重复次数等功能。

举个例子,假设我们有一个重复规则,要求每周一、周三和周五重复一次。我们可以使用rrule.js来实现这个规则:

ini 复制代码
import { RRule } from 'rrule';

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.WE, RRule.FR],
});

const eventDates = rule.all();

// 打印日期序列
eventDates.forEach((date) => {
  console.log(date);
});

在上述示例中,我们使用rrule.js创建了一个每周重复的规则,并指定重复的日期为周一、周三和周五。然后,我们使用rule.all()生成满足该规则的日期序列,并遍历打印出每个日期。

rrule同样支持使用字符串来描述重复规则,例如,我们想获取每年最后一个周五,可以使用

ini 复制代码
RRULE:FREQ=YEARLY;BYDAY=-1FR

rrule.jsrrule的 JavaScript 版本,适用于在浏览器端或 Node.js 环境中使用。它提供了与 Python 版本类似的功能,可以轻松处理复杂的重复规则,适用于构建日历应用程序、定期任务调度等场景。

逻辑实现

首先是重复规则,rrule支持7种重复参数,分别是年、月、周、日、时、分、秒。枚举定义如下所示。

rust 复制代码
pub enum Frequency {
    /// The recurrence occurs on a yearly basis.
    Yearly = 0,
    /// The recurrence occurs on a monthly basis.
    Monthly = 1,
    /// The recurrence occurs on a weekly basis.
    Weekly = 2,
    /// The recurrence occurs on a daily basis.
    Daily = 3,
    /// The recurrence occurs on an hourly basis.
    Hourly = 4,
    /// The recurrence occurs on a minutely basis.
    Minutely = 5,
    /// The recurrence occurs on a second basis.
    Secondly = 6,
}

在此基础上,还有8种基础控制参数(还有点其他的,但我没实现)

markdown 复制代码
- INTERVAL // 控制重复间隔,正整数
- COUNT // 生成日期的个数
- UNTIL // 生成日期的终止时间
- BYDAY // 控制星期几 如MO,FR,3TU,-2WE
- BYMONTH // 指定月份
- BYMONTHDAY // 指定每月几号,支持正负
- BYYEARDAY // 指定每年第几天,支持正负
- BYWEEKNO // 指定是第几周

然后有趣的就来了,即使在重复周期为DAILY的情况下,所有的参数都是可用的,哪怕用了BYYEARDAY=1。当我首次发现这个事情时,我的心情无比糟糕。。。

另外,例如BYDAY=-1FR,如果是按年重复,代表着一年最后一个周五;如果在此基础上补充一个BYMONTH,则代表着指定月份最后一个周五;如果改成按月重复,代表着每月最后一个周五;如果按周重复,则代表着每个周五。非常的无语。。但是很灵活

具体参数的解析规则就不具体介绍了,下面以按日和月解析来讲讲如何解析重复规则。虽然看着很简洁,但是这是我花了5个完整天总结出来的。另外,我没有实现时、分、秒的解析,毕竟前面4个已经很复杂了。

这里有一些通用的规则:

  1. 必须设置开始时间,否则会取当前时刻
  2. 必须设置Freq
  3. countuntil至少设置一个
  4. 截止时间不能小于开始时间

按日解析

  1. 初始化rrule里的参数,初始化当前时间索引curr,存储列表list
  2. 准备while循环,判断curr是否大于until,并且list长度小于count,不符合则到第9步
  3. 此重复规则下,BYDAY将忽略正负数;如果BYDAY存在并且curr不满足BYDAY的需求,前进interval天,回到第二步
  4. 如果BYMONTH存在并且curr不满足BYMONTH,前进,并回到第二步
  5. 如果BYMONTHDAY存在,计算该月所有符合BYMONTHDAY的时刻,并判断curr是否符合其一。如果不符合,前进,并回到第二步
  6. 如果BYYEARDAY存在,计算该月所有符合BYYEARDAY的时刻,并判断curr是否符合其一。如果不符合,前进,并回到第二步
  7. 如果BYWEEKNO存在,计算该curr是否符合某个weekno。如果不符合,前进,并回到第二步
  8. curr放入列表,返回第二步
  9. 结束循环,并返回列表

按月获取

按周和按日其实是差不多的,就不具体讲解了,现在说说按月。

  1. 初始化rrule里的参数,初始化当前时间索引curr,存储列表list
  2. 如果没有任何控制参数,则直接取每月中,开始时间所在的日,按月递增,并返回对应列表
  3. 构造按月解析的闭包
    1. 初始化临时存储数组list
    2. 如果有指定月份并且当天不属于指定月份之一,直接返回空数组
    3. 如果指定了BYYEARDAY,获取所有符合条件的时间,存储到vec_by_year_day
      1. 如果vec_by_year_day为空,直接返回空数组,结束本月循环,
      2. 否则将数据存储到list
    4. 如果指定了BYMONTHDAY,获取所有符合条件的时间,存储到vec_by_month_day
      1. 如果vec_by_month_day为空,直接返回空数组,结束本月循环,
      2. 如果list不为空,则将listvec_by_month_day取交集,结果为空则直接返回
      3. list为空,则将vec_by_month_day数据存储到list
    5. 如果指定了BYWEEKNO,获取所有符合条件的时间,存储到vec_by_weekno
      1. 如果vec_by_weekno为空,则直接返回空数组,结束本月循环
      2. 如果list不为空,则将listvec_by_weekno取交集,结果为空则直接返回
      3. list为空,则将vec_by_weekno数据存储到list
    6. 如果指定了BYDAY,获取所有符合条件的时间,存储到vec_by_day
      1. 如果vec_by_day为空,则直接返回空数组,结束本月循环
      2. 如果list不为空,则将listvec_by_day取交集,结果为空则直接返回
      3. list为空,则将vec_by_day数据存储到list
    7. 对临时数组list进行排序,并返回
  4. 准备while循环,判断curr是否大于until,并且list长度小于count
  5. 传入当前时间curr到第三步的闭包中,获取返回值
  6. 将返回值放入list中,curr步进interval控制的月份,重新执行第4步
  7. 解析完成

rrule的控制参数,本质上是获取+筛选,即先获取符合条件的值,再与其他的值做交集,最后剩下的就是需要的。在具体实现的过程中,增加了一些判断,在特定场景下,可以直接结束循环以节省性能。

关于时区

在实现解析规则的过程中,我们一直没有介绍关于时区的事情。并不是忘了,其实在解析过程中,我一直没用到时区。

解析本质上是对如20231023这种格式的时间进行重复,已知年月日,已经够解析了。当我们按照一定规则获取到时间数组后,以年月日为参数,初始化一个带有时区的时间戳就够了。

性能

标准的速度验证还没来得及做,就简单验证了一下。标准场景下,执行效率会比rrule.js快4到5倍,如果是加上时区,会快100倍左右。(最新的rrule.js对于时区的计算有些性能问题,不知道什么时候会修)。

WebAssembly

在现代Web开发中,WebAssembly(简称Wasm)已经成为一个备受关注的技术。WebAssembly是一种可移植、体积小、加载快速的二进制格式,旨在提供一种高效的执行环境。它是一种新的编译目标,可以将各种编程语言的代码编译成WebAssembly模块,这些模块可以在现代浏览器中直接运行。WebAssembly的设计目标是实现高性能、安全性和可移植性。

优势

  • 性能优异:相比传统的JavaScript代码,WebAssembly的执行速度更快,因为它是直接在底层虚拟机中运行的。这使得Web应用程序可以更高效地处理复杂的计算任务,例如图形渲染、物理模拟等。
  • 跨平台兼容:WebAssembly可以在几乎所有现代浏览器中运行,无论是桌面还是移动设备。这意味着开发者可以使用各种编程语言来编写Web应用程序,而不仅仅局限于JavaScript。
  • 安全性:WebAssembly运行在沙箱环境中,提供了良好的安全性。它使用了一系列安全措施,如内存隔离和沙箱限制,以防止恶意代码对系统的攻击。
  • 模块化:WebAssembly模块可以作为独立的组件进行开发和部署,这使得开发者可以更好地管理和维护代码库。此外,模块化的设计也为将来的性能优化和增量更新提供了便利。

使用场景

在Web开发中,可以使用WebAssembly来提高应用程序的性能和功能。以下是一些使用WebAssembly的常见场景:

  • 高性能计算:如果应用程序需要进行大量的数值计算、图像处理或者复杂的算法运算,可以将这部分代码编译成WebAssembly模块,以提高计算性能。
  • 游戏开发:WebAssembly可以用于创建高性能的HTML5游戏,通过将游戏逻辑编译成WebAssembly模块,可以实现更流畅的游戏体验。
  • 跨平台应用:使用WebAssembly可以实现跨平台的应用程序,无论是桌面还是移动设备,用户都可以通过浏览器来访问和使用。
  • 移植现有代码:如果你已经有用其他编程语言编写的代码,可以通过将其编译成WebAssembly模块,将其集成到现有的Web应用程序中,而无需重写整个应用程序。

用Rust编写WebAssembly

用Rust写WebAssembly还是很方便的,就好像新学语言时,在控制台打出Hello World! 那么简单。

  1. 准备运行环境
  • 安装Rust,这是一切的基础
  • 要构建包,我们需要一个额外的工具wasm-pack。这有助于将代码编译为WebAssembly,并生成在浏览器中使用的正确包。在终端输入以下命令:
perl 复制代码
cargo install wasm-pack
  1. 启动新项目
sql 复制代码
cargo new --lib hello-wasm

这将创建一个名为hello-wasm的新库,其中的目录结构为

css 复制代码
├── Cargo.toml
└── src
    └── lib.rs

其中Cargo.toml与npm的package.json类似,都是用来配置构建的文件。最终打出的WebAssembly的包,其中的package文件就是依据toml文件生成的。

  1. 配置

先安装下辅助工具

bash 复制代码
cargo add wasm_bindgen

然后修改lib.rs的文件

rust 复制代码
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str)-> String {
    format!("Hello, {}!", name)
}

再修改下toml文件,主要是补充下lib的参数

toml 复制代码
[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yourgithubusername/hello-wasm"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
  1. 打包

现在可以愉快的打包了

css 复制代码
wasm-pack build --target web --release

使用我们最开始下载的打包工具,指定目标为web模式。就会在pkg文件夹下,生成一个简单但是齐全的npm包了。连d.ts文件都补齐了。

sql 复制代码
├── Cargo.lock
├── Cargo.toml
├── index.html
├── pkg
│   ├── hello_wasm.d.ts
│   ├── hello_wasm.js
│   ├── hello_wasm_bg.wasm
│   ├── hello_wasm_bg.wasm.d.ts
│   └── package.json
├── src
│   └── lib.rs
└── target
    ├── CACHEDIR.TAG
    ├── release
    └── wasm32-unknown-unknown
  1. 使用

简单测试下~。在根路径新建一个index.html文件,放入以下内容。

html 复制代码
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <script type="module">
      import init, { greet } from "./pkg/hello_wasm.js";
      init().then(() => {
        console.log(greet("WebAssembly"));
      });
    </script>
  </body>
</html>

script中当做正常的包来引入,然后执行。记得先调用下init。然后找个server,或者在vs里下载个Live Server,启动服务打开页面。就能在页面控制台上看到Hello, WebAssembly!了。

完整的示例可以参考WebAssembly官方页面,也可以在Rust WebAssembly教程中自己实现一个简单的小游戏。

创作不易,点个赞吧~

相关推荐
m0_7482517215 分钟前
DataOps驱动数据集成创新:Apache DolphinScheduler & SeaTunnel on Amazon Web Services
前端·apache
珊珊来吃16 分钟前
EXCEL中给某一列数据加上双引号
java·前端·excel
胡西风_foxww43 分钟前
【ES6复习笔记】Spread 扩展运算符(8)
前端·笔记·es6·扩展·运算符·spread
小林爱1 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
跨境商城搭建开发1 小时前
一个服务器可以搭建几个网站?搭建一个网站的流程介绍
运维·服务器·前端·vue.js·mysql·npm·php
hhzz1 小时前
vue前端项目中实现电子签名功能(附完整源码)
前端·javascript·vue.js
秋雨凉人心1 小时前
上传npm包加强
开发语言·前端·javascript·webpack·npm·node.js
时清云2 小时前
【算法】 课程表
前端·算法·面试
NoneCoder2 小时前
CSS系列(37)-- Overscroll Behavior详解
前端·css
Nejosi_念旧2 小时前
使用Webpack构建NPM Library
前端·webpack·npm