feat: replace v8 with quickjs (#119)

* feat: replace v8 with quickjs

* fix format; fix pipelines

* fix: workflow

* fix: remove incorrect target in node windows pipeline
This commit is contained in:
stefan-gorules
2024-02-09 10:48:08 +01:00
committed by GitHub
parent 12e745144d
commit c281c55ef1
17 changed files with 1315 additions and 201 deletions
+37 -30
View File
@@ -33,30 +33,18 @@ jobs:
fail-fast: false
matrix:
settings:
- host: windows-latest
target: 'x86_64-pc-windows-msvc'
build: |
yarn lerna exec "yarn build --target x86_64-pc-windows-msvc" --concurrency 1 --stream --no-prefix
- host: macos-latest
target: 'x86_64-apple-darwin'
build: |
yarn build &&
rustup target add x86_64-apple-darwin
yarn lerna exec "yarn build --target x86_64-apple-darwin" --concurrency 1 --stream --no-prefix
strip -x ./*.node
- host: windows-latest
build: |
yarn build
target: 'x86_64-pc-windows-msvc'
- host: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: >-
set -e &&
rustup target add x86_64-unknown-linux-gnu &&
cd bindings/nodejs &&
yarn lerna exec "yarn build --target x86_64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix &&
strip ./*.node
# - host: ubuntu-latest
# target: 'x86_64-unknown-linux-musl'
# docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
# build: |
# cd bindings/nodejs &&
# yarn build
- host: macos-latest
target: 'aarch64-apple-darwin'
build: |
@@ -67,22 +55,41 @@ jobs:
export CFLAGS="-isysroot $SYSROOT -isystem $SYSROOT";
yarn lerna exec "yarn build --target aarch64-apple-darwin" --concurrency 1 --stream --no-prefix
strip -x ./*.node
- host: ubuntu-latest
target: aarch64-unknown-linux-gnu
target: 'x86_64-unknown-linux-gnu'
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: |
set -e
sudo apt-get update
sudo apt-get install -y patch
cd bindings/nodejs
rustup target add x86_64-unknown-linux-gnu
yarn lerna exec "yarn build --target x86_64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix
strip ./*.node
- host: ubuntu-latest
target: 'aarch64-unknown-linux-gnu'
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
build: >-
rustup target add aarch64-unknown-linux-gnu &&
cd bindings/nodejs &&
yarn lerna exec "yarn build --target aarch64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix &&
build: |
set -e
sudo apt-get update
sudo apt-get install -y patch
cd bindings/nodejs
rustup target add aarch64-unknown-linux-gnu
yarn lerna exec "yarn build --target aarch64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix
llvm-strip ./*.node
# - host: ubuntu-latest
# target: 'x86_64-unknown-linux-musl'
# build: |
# cd bindings/nodejs &&
# yarn build
# - host: ubuntu-latest
# target: 'aarch64-unknown-linux-musl'
# downloadTarget: 'aarch64-unknown-linux-musl'
# docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
# build: >-
# set -e &&
# rustup target add aarch64-unknown-linux-musl &&
# cd bindings/nodejs &&
# yarn lerna exec "yarn build --target aarch64-unknown-linux-musl" --concurrency 1 --stream --no-prefix
name: stable - ${{ matrix.settings.target }} - node@18
@@ -163,7 +170,7 @@ jobs:
target: 'x86_64-apple-darwin'
- host: windows-latest
target: 'x86_64-pc-windows-msvc'
node: ['14', '16', '18']
node: ['16', '18', '20']
runs-on: ${{ matrix.settings.host }}
steps:
@@ -207,7 +214,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: ['14', '16', '18']
node: ['16', '18', '20']
runs-on: ubuntu-latest
steps:
+7 -3
View File
@@ -38,13 +38,17 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
uses: stefan-gorules/maturin-action@main
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter -m bindings/python/Cargo.toml
sccache: 'true'
manylinux: auto
manylinux: 2_28
before-script-linux: |
apt update
apt install -y patch
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
+36 -1
View File
@@ -1,3 +1,9 @@
/* tslint:disable */
/* eslint-disable */
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
@@ -11,7 +17,7 @@ function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim();
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
@@ -231,6 +237,35 @@ switch (platform) {
loadError = e
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'zen-engine.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./zen-engine.linux-riscv64-musl.node')
} else {
nativeBinding = require('@gorules/zen-engine-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'zen-engine.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./zen-engine.linux-riscv64-gnu.node')
} else {
nativeBinding = require('@gorules/zen-engine-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
+2 -2
View File
@@ -19,11 +19,11 @@ bincode = { workspace = true, optional = true }
petgraph = { workspace = true }
serde_json = { workspace = true, features = ["arbitrary_precision"] }
serde = { workspace = true, features = ["derive"] }
serde_v8 = { version = "0.162" }
once_cell = { workspace = true }
fixedbitset = "0.4.2"
futures = { workspace = true }
v8 = { version = "0.83.1" }
rquickjs = { version = "0.4.3", features = ["macro", "loader", "rust-alloc"] }
itertools = "0.12.1"
zen-expression = { path = "../expression", version = "0.17.0" }
[dev-dependencies]
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -1,6 +1,9 @@
import 'dayjs';
import 'big';
const log = [];
const console = {
globalThis.console = {
log: (...args) => {
try {
log.push({
@@ -16,7 +19,7 @@ const console = {
}
};
const main = (input) => JSON.stringify({
output: handler(input, {moment: __GLOBAL__DAYJS, dayjs: __GLOBAL__DAYJS, Big: Big, env: __GLOBAL__ENV}),
globalThis.main = (input) => JSON.stringify({
output: handler(input, {moment: dayjs, dayjs: dayjs, Big: Big, env: {}}),
log,
});
+6 -2
View File
@@ -4,6 +4,7 @@ use crate::loader::DecisionLoader;
use crate::model::DecisionNodeKind;
use anyhow::{anyhow, Context};
use async_recursion::async_recursion;
use rquickjs::Runtime;
use std::ops::Deref;
use std::sync::Arc;
@@ -11,14 +12,16 @@ pub struct DecisionHandler<T: DecisionLoader> {
trace: bool,
loader: Arc<T>,
max_depth: u8,
js_runtime: Option<Runtime>,
}
impl<T: DecisionLoader> DecisionHandler<T> {
pub fn new(trace: bool, max_depth: u8, loader: Arc<T>) -> Self {
pub fn new(trace: bool, max_depth: u8, loader: Arc<T>, js_runtime: Option<Runtime>) -> Self {
Self {
trace,
loader,
max_depth,
js_runtime,
}
}
@@ -36,7 +39,8 @@ impl<T: DecisionLoader> DecisionHandler<T> {
loader: self.loader.clone(),
iteration: request.iteration + 1,
trace: self.trace,
})?;
})?
.with_runtime(self.js_runtime.clone());
let result = sub_tree
.evaluate(&request.input)
@@ -0,0 +1,89 @@
use itertools::Itertools;
use rquickjs::{Ctx, Error as QError, FromJs, Type, Value as QValue};
use serde_json::{json, Map, Number, Value};
#[derive(Debug)]
pub(crate) struct JsValue(pub(crate) Value);
impl<'js> FromJs<'js> for JsValue {
fn from_js(ctx: &Ctx<'js>, v: QValue<'js>) -> rquickjs::Result<Self> {
let computed_value = match v.type_of() {
Type::Uninitialized | Type::Undefined | Type::Null => Value::Null,
Type::Bool => Value::Bool(
v.as_bool()
.ok_or_else(|| QError::new_from_js("bool", "bool"))?,
),
Type::Int => Value::Number(Number::from(
v.as_int()
.ok_or_else(|| QError::new_from_js("int", "number"))?,
)),
Type::BigInt => Value::Number(Number::from(
v.as_big_int()
.map(|b| b.clone().to_i64().ok())
.flatten()
.ok_or_else(|| QError::new_from_js("bigint", "number"))?,
)),
Type::Float => Value::Number(
v.as_float()
.map(|n| Number::from_f64(n))
.flatten()
.ok_or_else(|| QError::new_from_js("float", "number"))?,
),
Type::String => Value::String(
v.as_string()
.map(|s| s.to_string().ok())
.flatten()
.ok_or_else(|| QError::new_from_js("string", "string"))?,
),
Type::Array => {
let arr = v
.as_array()
.ok_or_else(|| QError::new_from_js("array", "array"))?;
let js_arr: Vec<Value> = arr
.iter::<QValue>()
.map_ok(|n| JsValue::from_js(ctx, n.clone()).map(|js_val| js_val.0))
.flatten()
.try_collect()
.map_err(|_| QError::new_from_js("array", "array"))?;
Value::Array(js_arr)
}
Type::Object => {
let object = v
.as_object()
.ok_or_else(|| QError::new_from_js("object", "object"))?;
let js_object: Map<String, Value> = object
.props::<String, QValue>()
.map_ok(|(key, value)| {
JsValue::from_js(ctx, value.clone()).map(|js_val| (key, js_val.0))
})
.flatten()
.try_collect()
.map_err(|_| QError::new_from_js("object", "object"))?;
Value::Object(js_object)
}
Type::Exception => {
let exception = v
.as_exception()
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
let message = exception
.message()
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
let description = exception.to_string();
json!({ "message": message, "description": description })
}
Type::Function => json!("[Function]"),
Type::Module => json!("[Module]"),
Type::Constructor => json!("[Constructor]"),
Type::Symbol => json!("[Symbol]"),
Type::Unknown => json!("[Unknown]"),
};
Ok(JsValue(computed_value))
}
}
+23 -15
View File
@@ -1,27 +1,27 @@
use std::time::{Duration, Instant};
use anyhow::anyhow;
use std::time::Duration;
use rquickjs::Runtime;
use serde_json::json;
use serde_json::{json, Value};
use crate::handler::function::script::{EvaluateResponse, Script};
use crate::handler::function::script::Script;
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
use crate::model::DecisionNodeKind;
mod js_value;
pub(crate) mod runtime;
mod script;
mod vm;
async fn evaluate(source: &str, args: &Value) -> anyhow::Result<EvaluateResponse> {
let mut script = Script::new().with_timeout(Duration::from_millis(50));
script.call(source, args).await
}
pub struct FunctionHandler {
trace: bool,
runtime: Runtime,
}
static MAX_DURATION: Duration = Duration::from_millis(500);
impl FunctionHandler {
pub fn new(trace: bool) -> Self {
Self { trace }
pub fn new(trace: bool, runtime: Runtime) -> Self {
Self { trace, runtime }
}
pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult {
@@ -30,11 +30,19 @@ impl FunctionHandler {
_ => Err(anyhow!("Unexpected node type")),
}?;
let result = evaluate(content.as_str(), &request.input).await?;
let start = Instant::now();
let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION);
self.runtime.set_interrupt_handler(Some(interrupt_handler));
let mut script = Script::new(self.runtime.clone());
let result_response = script.call(content, &request.input).await;
self.runtime.set_interrupt_handler(None);
let response = result_response?;
Ok(NodeResponse {
output: result.output,
trace_data: self.trace.then(|| json!({ "log": result.log })),
output: response.output,
trace_data: self.trace.then(|| json!({ "log": response.log })),
})
}
}
@@ -0,0 +1,16 @@
use anyhow::Context;
use rquickjs::loader::Bundle;
use rquickjs::{embed, Runtime};
static JS_BUNDLE: Bundle = embed! {
"dayjs": "js/dayjs.js",
"big": "js/big.js",
"internals": "js/internals.js"
};
pub(crate) fn create_runtime() -> anyhow::Result<Runtime> {
let runtime = Runtime::new().context("Failed to create runtime")?;
runtime.set_loader(JS_BUNDLE, JS_BUNDLE);
Ok(runtime)
}
+29 -67
View File
@@ -1,12 +1,11 @@
use std::fmt::Debug;
use std::thread;
use std::time::Duration;
use anyhow::{anyhow, Context};
use anyhow::Context as _;
use rquickjs::{Context, Ctx, Error as QError, FromJs, Runtime};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::handler::function::vm::BASE_VM;
use crate::handler::function::js_value::JsValue;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -16,84 +15,47 @@ pub struct EvaluateResponse {
}
pub struct Script {
isolate: v8::OwnedIsolate,
timeout: Option<Duration>,
runtime: Runtime,
}
impl Script {
pub fn new() -> Self {
Self {
isolate: v8::Isolate::new(
v8::CreateParams::default().snapshot_blob(BASE_VM.as_slice()),
),
timeout: None,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
assert!(timeout > Duration::ZERO);
self.timeout = Some(timeout);
self
pub fn new(runtime: Runtime) -> Self {
Self { runtime }
}
pub async fn call<P>(&mut self, source: &str, args: &P) -> anyhow::Result<EvaluateResponse>
where
P: Serialize,
{
let handle = self.isolate.thread_safe_handle();
let runtime = &self.runtime;
let context = Context::full(&runtime).context("Failed to create context")?;
let args_str =
serde_json::to_string(args).context("Failed to serialize function arguments")?;
let js_code_source = format!("const now = Date.now(); main({});", args_str);
let json_response = context.with(|ctx| -> anyhow::Result<String> {
let _ = ctx
.clone()
.compile("main", "import 'internals'; globalThis.now = Date.now();")
.map_err(|e| map_js_error(&ctx, e))?;
let handle_scope = &mut v8::HandleScope::new(&mut self.isolate);
let context = v8::Context::new(handle_scope);
let scope = &mut v8::ContextScope::new(handle_scope, context);
let tc_scope = &mut v8::TryCatch::new(scope);
let _ = ctx
.globals()
.set("log", Vec::<isize>::new())
.map_err(|e| map_js_error(&ctx, e))?;
let src = v8::String::new(tc_scope, source).context("Failed to compile source code")?;
ctx.eval::<String, _>(format!("{source};main({args_str})"))
.map_err(|e| map_js_error(&ctx, e))
})?;
let js_src = v8::String::new(tc_scope, js_code_source.as_str())
.context("Failed to compile source code")?;
if let Some(timeout) = self.timeout {
thread::spawn(move || {
thread::sleep(timeout);
handle.terminate_execution();
});
}
let Some(src_script) = v8::Script::compile(tc_scope, src, None) else {
let exception = tc_scope.exception().context("Failed to load script")?;
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
};
if let None = src_script.run(tc_scope) {
let exception = tc_scope.exception().unwrap();
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
}
let Some(js_script) = v8::Script::compile(tc_scope, js_src, None) else {
let exception = tc_scope.exception().context("Failed to load script")?;
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
};
let Some(result) = js_script.run(tc_scope) else {
if tc_scope.has_terminated() {
return Err(anyhow!("Timeout exceeded"));
}
let exception = tc_scope
.exception()
.context("Failed to run loaded script")?;
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
};
let result_string: String =
serde_v8::from_v8(tc_scope, result).context("Failed to parse function result")?;
serde_json::from_str(result_string.as_str()).context("Failed to parse function result")
serde_json::from_str(json_response.as_str()).context("Failed to parse function result")
}
}
fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error {
let error = JsValue::from_js(&ctx, ctx.catch())
.map(|v| v.0)
.unwrap_or(Value::String(e.to_string()));
anyhow::Error::msg(error.to_string())
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-61
View File
@@ -1,61 +0,0 @@
use std::collections::HashMap;
use std::env;
use once_cell::sync::Lazy;
use v8::FunctionCodeHandling;
const EXPOSED_PREFIX: &'static str = "ZEN_EXPOSED_";
pub static BASE_VM: Lazy<Vec<u8>> = Lazy::new(|| {
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
let env_src_string = {
let mut env_vars = HashMap::new();
env::vars().for_each(|(key, value)| {
if let Some(mod_key) = key.strip_prefix(EXPOSED_PREFIX) {
env_vars.insert(mod_key.to_string(), value);
}
});
format!(
"const __GLOBAL__ENV = {}",
serde_json::to_string(&env_vars).unwrap()
)
};
let mut isolate = v8::Isolate::snapshot_creator(Default::default());
{
let handle_scope = &mut v8::HandleScope::new(&mut isolate);
let context = v8::Context::new(handle_scope);
let scope = &mut v8::ContextScope::new(handle_scope, context);
let dayjs_src = v8::String::new(scope, include_str!("scripts/dayjs.js")).unwrap();
let bigjs_src = v8::String::new(scope, include_str!("scripts/big.js")).unwrap();
let internal_src = v8::String::new(scope, include_str!("scripts/internals.js")).unwrap();
let env_src = v8::String::new(scope, env_src_string.as_str()).unwrap();
v8::Script::compile(scope, dayjs_src, None)
.unwrap()
.run(scope);
v8::Script::compile(scope, bigjs_src, None)
.unwrap()
.run(scope);
v8::Script::compile(scope, internal_src, None)
.unwrap()
.run(scope);
v8::Script::compile(scope, env_src, None)
.unwrap()
.run(scope);
scope.set_default_context(context);
}
isolate
.create_blob(FunctionCodeHandling::Keep)
.unwrap()
.to_vec()
});
+38 -8
View File
@@ -4,6 +4,7 @@ use std::time::Instant;
use anyhow::anyhow;
use petgraph::algo::is_cyclic_directed;
use rquickjs::Runtime;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::Value;
@@ -11,6 +12,7 @@ use thiserror::Error;
use crate::handler::decision::DecisionHandler;
use crate::handler::expression::ExpressionHandler;
use crate::handler::function::runtime::create_runtime;
use crate::handler::function::FunctionHandler;
use crate::handler::node::NodeRequest;
use crate::handler::table::zen::DecisionTableHandler;
@@ -25,6 +27,7 @@ pub struct DecisionGraph<'a, L: DecisionLoader> {
trace: bool,
max_depth: u8,
iteration: u8,
runtime: Option<Runtime>,
}
pub struct DecisionGraphConfig<'a, T: DecisionLoader> {
@@ -68,9 +71,26 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> {
trace: config.trace,
loader: config.loader.clone(),
max_depth: config.max_depth,
runtime: None,
})
}
pub(crate) fn with_runtime(mut self, runtime: Option<Runtime>) -> Self {
self.runtime = runtime;
self
}
fn get_or_insert_runtime(&mut self) -> anyhow::Result<Runtime> {
if let Some(runtime) = &self.runtime {
return Ok(runtime.clone());
}
let runtime = create_runtime()?;
self.runtime.replace(runtime.clone());
Ok(runtime)
}
pub fn validate(&self) -> Result<(), DecisionGraphValidationError> {
let input_count = self.node_kind_count(DecisionNodeKind::InputNode);
if input_count != 1 {
@@ -180,7 +200,12 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> {
});
}
DecisionNodeKind::FunctionNode { .. } => {
let res = FunctionHandler::new(self.trace)
let runtime = self.get_or_insert_runtime().map_err(|e| NodeError {
source: e.into(),
node_id: node.id.clone(),
})?;
let res = FunctionHandler::new(self.trace, runtime)
.handle(&node_request)
.await
.map_err(|e| NodeError {
@@ -199,13 +224,18 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> {
});
}
DecisionNodeKind::DecisionNode { .. } => {
let res = DecisionHandler::new(self.trace, self.max_depth, self.loader.clone())
.handle(&node_request)
.await
.map_err(|e| NodeError {
source: e.into(),
node_id: node.id.to_string(),
})?;
let res = DecisionHandler::new(
self.trace,
self.max_depth,
self.loader.clone(),
self.runtime.clone(),
)
.handle(&node_request)
.await
.map_err(|e| NodeError {
source: e.into(),
node_id: node.id.to_string(),
})?;
walker.set_node_data(nid, res.output.clone());
trace!({
+1 -1
View File
@@ -101,7 +101,7 @@ async fn engine_errors() {
match infinite_fn.unwrap_err().deref() {
EvaluationError::NodeError(e) => {
assert_eq!(e.node_id, "e0fd96d0-44dc-4f0e-b825-06e56b442d78");
assert_eq!(e.source.to_string(), "Timeout exceeded");
assert!(e.source.to_string().contains("interrupted"));
}
_ => assert!(false, "Wrong error type"),
}