Compare commits

..

2 Commits

Author SHA1 Message Date
0bebd544a3 Clippy, shut up man. 2024-03-22 02:56:05 -04:00
64c9de09aa Add batch support to current KCL implementation 2024-03-22 02:45:08 -04:00
13 changed files with 742 additions and 714 deletions

1159
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@ members = [
]
[workspace.dependencies]
kittycad = { path = "../../../kittycad.rs/kittycad", default-features = false, features = ["js", "requests"] }
kittycad = { version = "0.2.60", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }

View File

@ -14,7 +14,6 @@ keywords = ["kcl", "KittyCAD", "CAD"]
anyhow = { version = "1.0.81", features = ["backtrace"] }
async-recursion = "1.0.5"
async-trait = "0.1.77"
boxcar = "0.2.4"
chrono = "0.4.35"
clap = { version = "4.5.2", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"

View File

@ -75,7 +75,6 @@ pub async fn modify_ast_for_sketch(
// Let's get the path info.
let resp = engine
.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::PathGetInfo { path_id: sketch_id },
@ -100,7 +99,6 @@ pub async fn modify_ast_for_sketch(
for segment in &path_info.segments {
if let Some(command_id) = &segment.command_id {
let h = engine.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::CurveGetControlPoints { curve_id: *command_id },

View File

@ -3,6 +3,7 @@
use std::sync::{Arc, Mutex};
use crate::executor::SourceRange;
use anyhow::{anyhow, Result};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
@ -29,7 +30,6 @@ pub struct EngineConnection {
responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>,
tcp_read_handle: Arc<TcpReadHandle>,
socket_health: Arc<Mutex<SocketHealth>>,
batch: Arc<Mutex<Vec<WebSocketRequest>>>,
}
pub struct TcpRead {
@ -72,20 +72,113 @@ struct ToEngineReq {
request_sent: oneshot::Sender<Result<()>>,
}
fn is_cmd_with_return_values(cmd: &kittycad::types::ModelingCmd) -> bool {
let (kittycad::types::ModelingCmd::Export { .. }
| kittycad::types::ModelingCmd::Extrude { .. }
| kittycad::types::ModelingCmd::SketchModeDisable { .. }
| kittycad::types::ModelingCmd::ObjectBringToFront { .. }
| kittycad::types::ModelingCmd::SelectWithPoint { .. }
| kittycad::types::ModelingCmd::HighlightSetEntity { .. }
| kittycad::types::ModelingCmd::EntityGetChildUuid { .. }
| kittycad::types::ModelingCmd::EntityGetNumChildren { .. }
| kittycad::types::ModelingCmd::EntityGetParentId { .. }
| kittycad::types::ModelingCmd::EntityGetAllChildUuids { .. }
| kittycad::types::ModelingCmd::CameraDragMove { .. }
| kittycad::types::ModelingCmd::CameraDragEnd { .. }
| kittycad::types::ModelingCmd::DefaultCameraGetSettings { .. }
| kittycad::types::ModelingCmd::DefaultCameraZoom { .. }
| kittycad::types::ModelingCmd::SelectGet { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllEdgeFaces { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllOppositeEdges { .. }
| kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetNextAdjacentEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { .. }
| kittycad::types::ModelingCmd::GetEntityType { .. }
| kittycad::types::ModelingCmd::CurveGetControlPoints { .. }
| kittycad::types::ModelingCmd::CurveGetType { .. }
| kittycad::types::ModelingCmd::MouseClick { .. }
| kittycad::types::ModelingCmd::TakeSnapshot { .. }
| kittycad::types::ModelingCmd::PathGetInfo { .. }
| kittycad::types::ModelingCmd::PathGetCurveUuidsForVertices { .. }
| kittycad::types::ModelingCmd::PathGetVertexUuids { .. }
| kittycad::types::ModelingCmd::CurveGetEndPoints { .. }
| kittycad::types::ModelingCmd::FaceIsPlanar { .. }
| kittycad::types::ModelingCmd::FaceGetPosition { .. }
| kittycad::types::ModelingCmd::FaceGetGradient { .. }
| kittycad::types::ModelingCmd::PlaneIntersectAndProject { .. }
| kittycad::types::ModelingCmd::ImportFiles { .. }
| kittycad::types::ModelingCmd::Mass { .. }
| kittycad::types::ModelingCmd::Volume { .. }
| kittycad::types::ModelingCmd::Density { .. }
| kittycad::types::ModelingCmd::SurfaceArea { .. }
| kittycad::types::ModelingCmd::CenterOfMass { .. }
| kittycad::types::ModelingCmd::GetSketchModePlane { .. }
| kittycad::types::ModelingCmd::EntityGetDistance { .. }
| kittycad::types::ModelingCmd::EntityLinearPattern { .. }
| kittycad::types::ModelingCmd::EntityCircularPattern { .. }
| kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { .. }) = cmd
else {
return false;
};
true
}
impl EngineConnection {
/// Start waiting for incoming engine requests, and send each one over the WebSocket to the engine.
async fn start_write_actor(mut tcp_write: WebSocketTcpWrite, mut engine_req_rx: mpsc::Receiver<ToEngineReq>) {
let mut batch: Vec<kittycad::types::ModelingCmdReq> = vec![];
while let Some(req) = engine_req_rx.recv().await {
let ToEngineReq { req, request_sent } = req;
let res = if let kittycad::types::WebSocketRequest::ModelingCmdReq {
cmd: kittycad::types::ModelingCmd::ImportFiles { .. },
cmd_id: _,
} = &req
{
let kittycad::types::WebSocketRequest::ModelingCmdReq { cmd, cmd_id } = &req else {
return;
};
let res = if let kittycad::types::ModelingCmd::ImportFiles { .. } = cmd {
// Send it as binary.
Self::inner_send_to_engine_binary(req, &mut tcp_write).await
} else {
Self::inner_send_to_engine(req, &mut tcp_write).await
// Backported from the new Grackle-core KCL.
// We will batch all commands until we hit one which has
// return values we want to wait for. Currently, that means
// waiting for API requests with return values that are not just
// request confirmations (ex. face or edge data).
batch.push(kittycad::types::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
});
if is_cmd_with_return_values(cmd) || *cmd_id == uuid::Uuid::nil() {
// If the batch has zero commands, and we're about to load
// a command that has return values, don't wrap it in a
// ModelingCmdBatchReq.
let future = if batch.len() == 1 {
Self::inner_send_to_engine(
kittycad::types::WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
},
&mut tcp_write,
)
} else {
// Serde will properly serialize these.
Self::inner_send_to_engine(
kittycad::types::WebSocketRequest::ModelingCmdBatchReq {
requests: batch.clone(),
},
&mut tcp_write,
)
};
// Prepare for a new batch of instructions.
batch.clear();
future.await
} else {
Ok(())
}
};
let _ = request_sent.send(res);
}
@ -155,136 +248,27 @@ impl EngineConnection {
}),
responses,
socket_health,
batch: Arc::new(Mutex::new(Vec::new())),
})
}
}
fn is_cmd_with_return_values(cmd: &kittycad::types::ModelingCmd) -> bool {
let (kittycad::types::ModelingCmd::Export { .. }
| kittycad::types::ModelingCmd::Extrude { .. }
| kittycad::types::ModelingCmd::SketchModeDisable { .. }
| kittycad::types::ModelingCmd::ObjectBringToFront { .. }
| kittycad::types::ModelingCmd::SelectWithPoint { .. }
| kittycad::types::ModelingCmd::HighlightSetEntity { .. }
| kittycad::types::ModelingCmd::EntityGetChildUuid { .. }
| kittycad::types::ModelingCmd::EntityGetNumChildren { .. }
| kittycad::types::ModelingCmd::EntityGetParentId { .. }
| kittycad::types::ModelingCmd::EntityGetAllChildUuids { .. }
| kittycad::types::ModelingCmd::CameraDragMove { .. }
| kittycad::types::ModelingCmd::CameraDragEnd { .. }
| kittycad::types::ModelingCmd::DefaultCameraGetSettings { .. }
| kittycad::types::ModelingCmd::DefaultCameraZoom { .. }
| kittycad::types::ModelingCmd::SelectGet { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllEdgeFaces { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllOppositeEdges { .. }
| kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetNextAdjacentEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { .. }
| kittycad::types::ModelingCmd::GetEntityType { .. }
| kittycad::types::ModelingCmd::CurveGetControlPoints { .. }
| kittycad::types::ModelingCmd::CurveGetType { .. }
| kittycad::types::ModelingCmd::MouseClick { .. }
| kittycad::types::ModelingCmd::TakeSnapshot { .. }
| kittycad::types::ModelingCmd::PathGetInfo { .. }
| kittycad::types::ModelingCmd::PathGetCurveUuidsForVertices { .. }
| kittycad::types::ModelingCmd::PathGetVertexUuids { .. }
| kittycad::types::ModelingCmd::CurveGetEndPoints { .. }
| kittycad::types::ModelingCmd::FaceIsPlanar { .. }
| kittycad::types::ModelingCmd::FaceGetPosition { .. }
| kittycad::types::ModelingCmd::FaceGetGradient { .. }
| kittycad::types::ModelingCmd::PlaneIntersectAndProject { .. }
| kittycad::types::ModelingCmd::ImportFiles { .. }
| kittycad::types::ModelingCmd::Mass { .. }
| kittycad::types::ModelingCmd::Volume { .. }
| kittycad::types::ModelingCmd::Density { .. }
| kittycad::types::ModelingCmd::SurfaceArea { .. }
| kittycad::types::ModelingCmd::CenterOfMass { .. }
| kittycad::types::ModelingCmd::GetSketchModePlane { .. }
| kittycad::types::ModelingCmd::EntityGetDistance { .. }
| kittycad::types::ModelingCmd::EntityLinearPattern { .. }
| kittycad::types::ModelingCmd::EntityCircularPattern { .. }
| kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { .. }) = cmd
else {
return false;
};
true
}
#[async_trait::async_trait]
impl EngineManager for EngineConnection {
async fn send_modeling_cmd(
&self,
flush_batch: bool,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
source_range: SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
let req = WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id,
};
println!("req {:?}", req);
if !flush_batch {
self.batch.lock().unwrap().push(req);
}
// If the batch only has this one command that expects a return value,
// fire it right away, or if we want to flush batch queue.
let is_sending = (is_cmd_with_return_values(&cmd) && self.batch.lock().unwrap().len() == 1)
|| flush_batch
|| is_cmd_with_return_values(&cmd);
// Return a fake modeling_request empty response.
if !is_sending {
println!("fake {:?}", cmd);
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {}
});
}
let batched_requests =
WebSocketRequest::ModelingCmdBatchReq {
requests: self.batch.lock().unwrap().iter().fold(vec![], |mut acc, val| {
let WebSocketRequest::ModelingCmdReq { cmd, cmd_id } = val else { return acc; };
acc.push(kittycad::types::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
});
acc
}),
batch_id: uuid::Uuid::new_v4()
};
let final_req = if self.batch.lock().unwrap().len() == 1 {
self.batch.lock().unwrap().get(0).unwrap().clone()
} else {
batched_requests
};
// Throw away the old batch queue.
self.batch.lock().unwrap().clear();
println!("final req {:?}", final_req);
// We pop off the responses to cleanup our mappings.
let id_final = match final_req {
WebSocketRequest::ModelingCmdBatchReq { requests: _, batch_id } => batch_id,
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => cmd_id,
_ => panic!("should not be possible"),
};
let (tx, rx) = oneshot::channel();
// Send the request to the engine, via the actor.
self.engine_req_tx
.send(ToEngineReq {
req: final_req.clone(),
req: WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id,
},
request_sent: tx,
})
.await
@ -310,8 +294,18 @@ impl EngineManager for EngineConnection {
})
})?;
// Only wait for a response if it's a command *with* return values
// So most of the time, this condition will be true.
if !is_cmd_with_return_values(&cmd) {
// Simulate an empty response type
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
});
}
// If it's a submitted batch, we want to check the batch return value
// in case of any errors.
// Wait for the response.
let current_time = std::time::Instant::now();
while current_time.elapsed().as_secs() < 60 {
if let Ok(guard) = self.socket_health.lock() {
@ -322,9 +316,8 @@ impl EngineManager for EngineConnection {
}));
}
}
if let Some((_, resp)) = self.responses.remove(&id_final) {
println!("RESP {:?}", resp);
// We pop off the responses to cleanup our mappings.
if let Some((_, resp)) = self.responses.remove(&id) {
return if let Some(data) = &resp.resp {
Ok(data.clone())
} else {
@ -337,7 +330,7 @@ impl EngineManager for EngineConnection {
}
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command timed out `{}`", id_final),
message: format!("Modeling command timed out `{}`", id),
source_ranges: vec![source_range],
}))
}

View File

@ -19,7 +19,6 @@ impl EngineConnection {
impl crate::engine::EngineManager for EngineConnection {
async fn send_modeling_cmd(
&self,
_flush_batch: bool,
_id: uuid::Uuid,
_source_range: crate::executor::SourceRange,
_cmd: kittycad::types::ModelingCmd,

View File

@ -10,10 +10,27 @@ pub mod conn_wasm;
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Tell the EngineManager there will be no more commands.
/// We send a "dummy command" to signal EngineConnection that we're
/// at the end of the program and there'll be no more requests.
/// This means in tests, where it's impossible to look ahead, we'll need to
/// add this to mark the end of commands.
/// In compiled KCL tests, it will be auto-inserted.
async fn signal_end(&self) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
self.send_modeling_cmd(
// THE NIL UUID IS THE SIGNAL OF THE END OF TIMES FOR THIS POOR PROGRAM.
uuid::Uuid::nil(),
// This will be ignored.
crate::executor::SourceRange([0, 0]),
// This will be ignored. It was one I found with no fields.
kittycad::types::ModelingCmd::EditModeExit {},
)
.await
}
/// Send a modeling command and wait for the response message.
async fn send_modeling_cmd(
&self,
flush_batch: bool,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,

View File

@ -1008,7 +1008,6 @@ pub async fn execute(
// Before we even start executing the program, set the units.
ctx.engine
.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
SourceRange::default(),
kittycad::types::ModelingCmd::SetSceneUnits {
@ -1220,16 +1219,8 @@ pub async fn execute(
}
}
// Fire the batch since we've reached the end.
// ctx.engine
// .send_modeling_cmd(
// true,
// uuid::Uuid::new_v4(),
// SourceRange::default(),
// // This is ignored when flush_batch is true.
// kittycad::types::ModelingCmd::EditModeExit {},
// )
// .await?;
// Signal to engine we're done. Flush the batch.
ctx.engine.signal_end().await?;
Ok(memory.clone())
}

View File

@ -165,6 +165,7 @@ async fn inner_get_opposite_edge(tag: String, extrude_group: Box<ExtrudeGroup>,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetOppositeEdge { data: opposite_edge },
} = &resp

View File

@ -206,7 +206,7 @@ impl Args {
id: uuid::Uuid,
cmd: kittycad::types::ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
self.ctx.engine.send_modeling_cmd(false, id, self.source_range, cmd).await
self.ctx.engine.send_modeling_cmd(id, self.source_range, cmd).await
}
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {

View File

@ -915,7 +915,7 @@ async fn start_sketch_on_face(
})
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a face with the tag `{}`", tag),
message: format!("Expected a face with the tag `{}` for sketch", tag),
source_ranges: vec![args.source_range],
})
})??,

View File

@ -21,9 +21,9 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
let client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// uncomment to use a local server
client.set_base_url("http://localhost:8080/");
//client.set_base_url("http://system76-pc:8080/");
let ws = client
.modeling()
@ -45,7 +45,6 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
ctx.engine
.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
kcl_lib::executor::SourceRange::default(),
kittycad::types::ModelingCmd::DefaultCameraLookAt {
@ -61,7 +60,6 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
let resp = ctx
.engine
.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
kcl_lib::executor::SourceRange::default(),
kittycad::types::ModelingCmd::TakeSnapshot {

View File

@ -49,7 +49,6 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let plane_id = uuid::Uuid::new_v4();
ctx.engine
.send_modeling_cmd(
false,
plane_id,
SourceRange::default(),
ModelingCmd::MakePlane {
@ -68,7 +67,6 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
// You can however get path info without sketch mode.
ctx.engine
.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::SketchModeEnable {
@ -84,7 +82,6 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
// We can't get control points of an existing sketch without being in edit mode.
ctx.engine
.send_modeling_cmd(
false,
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::EditModeEnter { target: sketch_id },