Skip to content

Commit

Permalink
WASM bind
Browse files Browse the repository at this point in the history
  • Loading branch information
vkartaviy committed Jun 13, 2024
1 parent 8402921 commit 5292117
Show file tree
Hide file tree
Showing 14 changed files with 613 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/target
Cargo.lock
.idea
.DS_Store
12 changes: 10 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rust-version = "1.74.0"
[workspace]
members = [
"rrule",
"rrule-debugger",
"rrule-debugger"
]
resolver = "2"

Expand All @@ -20,4 +20,12 @@ overflow-checks = true

[profile.release]
# Always have overflow checks until crate is stable, see roadmap.
overflow-checks = true
overflow-checks = false
# Optimize for size
opt-level = "z"
# Enable Link Time Optimization
lto = true
# Reduce the number of codegen units to increase optimization
codegen-units = 1
# Disable debug info
debug = false
15 changes: 13 additions & 2 deletions rrule/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ regex = { version = "1.5.5", default-features = false, features = ["perf", "std"
clap = { version = "4.1.9", optional = true, features = ["derive"] }
thiserror = "1.0.30"
serde_with = { version = "3.8.1", optional = true }
wasm-bindgen = { version="0.2.92", optional = true }
js-sys = { version="0.3.69", optional = true }
wee_alloc = { version="0.4.1", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }

[dev-dependencies]
serde_json = "1.0.80"
orig_serde = { package = "serde", version = "1.0.137", default-features = false, features = ["derive"]}
wasm-bindgen-test = "0.3.42"

[[bin]]
name = "rrule"
required-features = ["cli-tool"]

[features]
default = []
default = ["wasm", "wee_alloc", "console_error_panic_hook"]

# Allows the enabling of the `by_easter` field and `BYEASTER` parser.
by-easter = []
Expand All @@ -43,4 +48,10 @@ cli-tool = ["clap"]
serde = ["serde_with", "chrono/serde", "chrono-tz/serde"]

# Allows EXRULE's to be used in the `RRuleSet`.
exrule = []
exrule = []

# Allows to use WASM
wasm = ["dep:wasm-bindgen", "dep:js-sys"]

[lib]
crate-type = ["cdylib", "rlib"]
20 changes: 20 additions & 0 deletions rrule/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
install-wasm-pack:
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

build-wasm-nodejs:
wasm-pack build --release --target nodejs --out-dir pkg/nodejs --features "wasm"

test-wasm-on-nodejs:
node examples/wasm/nodejs/app.js

build-wasm-web:
wasm-pack build --release --target web --out-dir pkg/web --features "wasm"

test-wasm-on-web-browser:
npx http-server -o /examples/wasm/web/index.html

build-wasm-bundler:
wasm-pack build --release --target bundler --features "wasm"

pack:
wasm-pack pack pkg
11 changes: 11 additions & 0 deletions rrule/examples/wasm/nodejs/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { get_all_date_recurrences_between } = require('../../../pkg/nodejs/rrule.js');

const rule_set = 'DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY';
const data = get_all_date_recurrences_between(
rule_set,
10,
new Date(2021, 0, 1),
new Date(2023, 0, 1)
);

console.log(data);
1 change: 1 addition & 0 deletions rrule/examples/wasm/web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
144 changes: 144 additions & 0 deletions rrule/examples/wasm/web/benchmarking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import init, { getAllRecurrencesBetween } from '../../../pkg/web/rrule.js';
import { tryParseEventRecurrenceRules, createValidDateTimeFromISO, getInstanceStartAt } from './rrule_utils.js';

function executeRRulePerformanceTest(ruleSet, after, before, limit) {
var rruleWork = () => {
const rule = new rrule.rrulestr(ruleSet);
const results = rule.between(after, before);
}
return executeWork(rruleWork, "rrule");
}
async function executeRustRRulePerformanceTest(ruleSet, after, before, limit) {
var rustWork = () => {
const data = getAllRecurrencesBetween(ruleSet, after, before, limit);
}
return executeWork(rustWork, "rust-rrule");
}

const performance = window.performance;

function executeWork(work, framework, rounds = 100) {
const measurements = [];

for (let round = 0; round < rounds; round++) {
const t0 = performance.now();
work();
const t1 = performance.now();
measurements.push(t1 - t0);
}

// Calculate mean
const mean = measurements.reduce((a, b) => a + b, 0) / measurements.length;

// Calculate standard deviation
const standardDeviation = Math.sqrt(
measurements.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / measurements.length
);

// Calculate confidence interval (95% confidence level)
const zScore = 1.96; // Z-score for 95% confidence
const marginOfError = zScore * (standardDeviation / Math.sqrt(measurements.length));
const confidenceInterval = [mean - marginOfError, mean + marginOfError];

return `Call to ${framework} took an average of ${mean.toFixed(2)} milliseconds with a 95% confidence interval of (${confidenceInterval[0].toFixed(2)}, ${confidenceInterval[1].toFixed(2)}) milliseconds.`;
}

async function executePerformanceTests() {
const ruleSet = document.getElementById("ruleSet").value.replaceAll('\\n', '\n');
const afterDateString = document.getElementById("after").value;
const beforeDateString = document.getElementById("before").value;
const limit = document.getElementById("limit").value;
let after = new Date(afterDateString);
let before = new Date(beforeDateString)

const wasmInitTimeDiv = document.querySelector("#wasmInitTime");

wasmInitTimeDiv.innerHTML = "Loading ...";
const t0 = performance.now();
await init();
const t1 = performance.now();
wasmInitTimeDiv.innerHTML = (t1 - t0) + " milliseconds.";

const rustRRuleResultDiv = document.querySelector("#rustRRuleResult");
rustRRuleResultDiv.innerHTML = "Executing ...";
rustRRuleResultDiv.innerHTML = await executeRustRRulePerformanceTest(ruleSet, after, before, limit);

setTimeout(() => {
const rruleResultDiv = document.querySelector("#rruleResult");
rruleResultDiv.innerHTML = "Executing ...";
rruleResultDiv.innerHTML = executeRRulePerformanceTest(ruleSet, after, before, limit);

const matchErrorsDiv = document.querySelector("#matchErrors");

// const sourceEvent = {
// startAt: '2023-05-31T20:00:00+05:30',
// startTimeZone: 'Asia/Kolkata',
// recurrence: [
// // 'DTSTART;TZID=Asia/Kolkata:20230531T200000',
// 'EXDATE;TZID=Asia/Kolkata:20230810T200000',
// 'RRULE:FREQ=DAILY;UNTIL=20230818T143000Z',
// ],
// };
//
// after = new Date('2023-05-30T00:00:00Z');
// before = new Date('2023-09-01T00:00:00Z');

const sourceEvent = {
startAt: '2019-08-13T15:30:00',
startTimeZone: 'Europe/Moscow',
recurrence: [
// 'DTSTART;TZID=Europe/Moscow:20190813T153000',
'RRULE:FREQ=DAILY',
],
};

after = new Date('2019-06-05T21:00:00Z');
before = new Date('2022-06-22T20:59:59Z');

const event = {
recurrenceRules: sourceEvent.recurrence,
startAt: createValidDateTimeFromISO(sourceEvent.startAt, {
zone: sourceEvent.startTimeZone,
}),
}

const rruleSet = tryParseEventRecurrenceRules(event, { useStartDate: true })

console.log(rruleSet.toString());

const dates1 = getAllRecurrencesBetween([
// `DTSTART;TZID=Asia/Kolkata:20230531T200000`,
'DTSTART;TZID=Europe/Moscow:20190813T153000',
...sourceEvent.recurrence].join('\n'), after, before, limit);
const dates2 = rruleSet.between(after, before);

console.log(dates1, dates2);

let isFullMatch = true;

for (let i = 0; i < dates1.length; i++) {
let d1 = dates1.at(i);
let d2 = dates2.at(i);

d1 = d1 ? new Date(d1) : null;
d2 = getInstanceStartAt(d2, event.startAt).toJSDate();

if (d1?.getTime() !== d2?.getTime()) {
matchErrorsDiv.innerHTML += `<li>Dates do not match at index ${i}: ${d1?.toISOString()} !== ${d2?.toISOString()}</li>`;
isFullMatch = false;
}
}

if (isFullMatch) {
matchErrorsDiv.innerHTML = `All dates match! (${dates1.length})`;
}
});
}

document.addEventListener("DOMContentLoaded", () => {
const performanceButton = document.querySelector("#performanceButton");

performanceButton.addEventListener("click", () => {
executePerformanceTests();
});
});
40 changes: 40 additions & 0 deletions rrule/examples/wasm/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<script type="module" src="./node_modules/rrule/dist/es5/rrule.js"></script>
<script type="module" src="./benchmarking.js"></script>
</head>
<body>
<h1>rust-rrule x rrule</h1>

<label for="ruleSet">RuleSet:</label>
<textarea id="ruleSet" name="ruleSet" cols="150" rows="8">
DTSTART;TZID=Europe/Moscow:20190813T153000
RRULE:FREQ=DAILY
RDATE;TZID=Europe/Moscow:20190813T153000
</textarea>

<br><br>

<label for="after">After:</label>
<input type="text" id="after" name="after" value="2019-06-05T21:00:00Z"><br><br>

<label for="before">Before:</label>
<input type="text" id="before" name="before" value="2022-06-22T20:59:59Z"><br><br>

<label for="limit">Date Recurrences Limit:</label>
<input type="text" id="limit" name="limit" value="730"><br><br>

<button id="performanceButton">Calculate Performance</button>

<br><br><br>
<label>Result:</label>
<div>WASM init time: <span id="wasmInitTime">...</span></div>
<div id="rustRRuleResult">No rust-rrule results yet</div>
<div id="rruleResult">No rrule results yet</div>
<div>Match: <span id="matchErrors">...</span></div>
</body>
</html>
7 changes: 7 additions & 0 deletions rrule/examples/wasm/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "rrule-wasm",
"dependencies": {
"rrule": "latest",
"luxon": "latest"
}
}
Loading

0 comments on commit 5292117

Please sign in to comment.