Fix execution caching to cache artifact graph NodePath (#6978)

* Fix to add NodePaths to SketchOnFace and SketchOnPlane artifacts

* Fix to only compute the new part of the artifact graph

* Change to early-return sooner when in mock mode

* Add another test

* Fix to propagate NodePath for sketch on face

* Update output
This commit is contained in:
Jonathan Tran
2025-05-15 19:18:03 -04:00
committed by GitHub
parent 9906c9947a
commit 74939e5cd6
63 changed files with 357 additions and 274 deletions

View File

@ -115,7 +115,7 @@ where
seq.end()
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq, ts_rs::TS)]
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct CodeRef {
@ -396,7 +396,6 @@ pub enum Artifact {
Cap(Cap),
SweepEdge(SweepEdge),
EdgeCut(EdgeCut),
#[expect(unused)]
EdgeCutEdge(EdgeCutEdge),
Helix(Helix),
}
@ -550,8 +549,9 @@ impl Artifact {
}
}
#[expect(dead_code)]
pub(crate) fn code_ref(&self) -> Option<&CodeRef> {
/// The [`CodeRef`] for the artifact itself. See also
/// [`Self::face_code_ref`].
pub fn code_ref(&self) -> Option<&CodeRef> {
match self {
Artifact::CompositeSolid(a) => Some(&a.code_ref),
Artifact::Plane(a) => Some(&a.code_ref),
@ -570,6 +570,24 @@ impl Artifact {
}
}
/// The [`CodeRef`] referring to the face artifact that it's on, not the
/// artifact itself.
pub fn face_code_ref(&self) -> Option<&CodeRef> {
match self {
Artifact::CompositeSolid(_)
| Artifact::Plane(_)
| Artifact::Path(_)
| Artifact::Segment(_)
| Artifact::Solid2d(_)
| Artifact::StartSketchOnFace(_)
| Artifact::StartSketchOnPlane(_)
| Artifact::Sweep(_) => None,
Artifact::Wall(a) => Some(&a.face_code_ref),
Artifact::Cap(a) => Some(&a.face_code_ref),
Artifact::SweepEdge(_) | Artifact::EdgeCut(_) | Artifact::EdgeCutEdge(_) | Artifact::Helix(_) => None,
}
}
/// Merge the new artifact into self. If it can't because it's a different
/// type, return the new artifact which should be used as a replacement.
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
@ -704,6 +722,19 @@ impl ArtifactGraph {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn values(&self) -> impl Iterator<Item = &Artifact> {
self.map.values()
}
/// Consume the artifact graph and return the map of artifacts.
fn into_map(self) -> IndexMap<ArtifactId, Artifact> {
self.map
}
/// Used to make the mermaid tests deterministic.
#[cfg(test)]
pub(crate) fn sort(&mut self) {
@ -712,17 +743,29 @@ impl ArtifactGraph {
}
}
/// Build the artifact graph from the artifact commands and the responses. The
/// initial graph is the graph cached from a previous execution. NodePaths of
/// `exec_artifacts` are filled in from the AST.
pub(super) fn build_artifact_graph(
artifact_commands: &[ArtifactCommand],
responses: &IndexMap<Uuid, WebSocketResponse>,
ast: &Node<Program>,
exec_artifacts: &IndexMap<ArtifactId, Artifact>,
exec_artifacts: &mut IndexMap<ArtifactId, Artifact>,
initial_graph: ArtifactGraph,
) -> Result<ArtifactGraph, KclError> {
let mut map = IndexMap::new();
let mut map = initial_graph.into_map();
let mut path_to_plane_id_map = FnvHashMap::default();
let mut current_plane_id = None;
// Fill in NodePaths for artifacts that were added directly to the map
// during execution.
for exec_artifact in exec_artifacts.values_mut() {
// Note: We only have access to the new AST. So if these artifacts
// somehow came from cached AST, this won't fill in anything.
fill_in_node_paths(exec_artifact, ast);
}
for artifact_command in artifact_commands {
if let ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) = artifact_command.command {
current_plane_id = Some(entity_id);
@ -762,6 +805,24 @@ pub(super) fn build_artifact_graph(
Ok(ArtifactGraph { map })
}
/// These may have been created with placeholder `CodeRef`s because we didn't
/// have the entire AST available. Now we fill them in.
fn fill_in_node_paths(artifact: &mut Artifact, program: &Node<Program>) {
match artifact {
Artifact::StartSketchOnFace(face) => {
if face.code_ref.node_path.is_empty() {
face.code_ref.node_path = NodePath::from_range(program, face.code_ref.range).unwrap_or_default();
}
}
Artifact::StartSketchOnPlane(plane) => {
if plane.code_ref.node_path.is_empty() {
plane.code_ref.node_path = NodePath::from_range(program, plane.code_ref.range).unwrap_or_default();
}
}
_ => {}
}
}
/// Flatten the responses into a map of command IDs to modeling command
/// responses. The raw responses from the engine contain batches.
fn flatten_modeling_command_responses(
@ -846,6 +907,12 @@ fn artifacts_to_update(
ast: &Node<Program>,
exec_artifacts: &IndexMap<ArtifactId, Artifact>,
) -> Result<Vec<Artifact>, KclError> {
let uuid = artifact_command.cmd_id;
let Some(response) = responses.get(&uuid) else {
// Response not found or not successful.
return Ok(Vec::new());
};
// TODO: Build path-to-node from artifact_command source range. Right now,
// we're serializing an empty array, and the TS wrapper fills it in with the
// correct value based on NodePath.
@ -858,14 +925,7 @@ fn artifacts_to_update(
path_to_node,
};
let uuid = artifact_command.cmd_id;
let id = ArtifactId::new(uuid);
let Some(response) = responses.get(&uuid) else {
// Response not found or not successful.
return Ok(Vec::new());
};
let cmd = &artifact_command.command;
match cmd {
@ -1100,16 +1160,19 @@ fn artifacts_to_update(
let extra_artifact = exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace(s) = a {
s.face_id == face_id
} else if let Artifact::StartSketchOnPlane(s) = a {
s.plane_id == face_id
} else {
false
}
});
let sketch_on_face_source_range = extra_artifact
let sketch_on_face_code_ref = extra_artifact
.and_then(|a| match a {
Artifact::StartSketchOnFace(s) => Some(s.code_ref.range),
// TODO: If we didn't find it, it's probably a bug.
Artifact::StartSketchOnFace(s) => Some(s.code_ref.clone()),
Artifact::StartSketchOnPlane(s) => Some(s.code_ref.clone()),
_ => None,
})
// TODO: If we didn't find it, it's probably a bug.
.unwrap_or_default();
return_arr.push(Artifact::Wall(Wall {
@ -1118,11 +1181,7 @@ fn artifacts_to_update(
edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id,
path_ids: Vec::new(),
face_code_ref: CodeRef {
range: sketch_on_face_source_range,
node_path: NodePath::from_range(ast, sketch_on_face_source_range).unwrap_or_default(),
path_to_node: Vec::new(),
},
face_code_ref: sketch_on_face_code_ref,
cmd_id: artifact_command.cmd_id,
}));
let mut new_seg = seg.clone();
@ -1155,15 +1214,19 @@ fn artifacts_to_update(
let extra_artifact = exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace(s) = a {
s.face_id == face_id
} else if let Artifact::StartSketchOnPlane(s) = a {
s.plane_id == face_id
} else {
false
}
});
let sketch_on_face_source_range = extra_artifact
let sketch_on_face_code_ref = extra_artifact
.and_then(|a| match a {
Artifact::StartSketchOnFace(s) => Some(s.code_ref.range),
Artifact::StartSketchOnFace(s) => Some(s.code_ref.clone()),
Artifact::StartSketchOnPlane(s) => Some(s.code_ref.clone()),
_ => None,
})
// TODO: If we didn't find it, it's probably a bug.
.unwrap_or_default();
return_arr.push(Artifact::Cap(Cap {
id: face_id,
@ -1171,11 +1234,7 @@ fn artifacts_to_update(
edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id,
path_ids: Vec::new(),
face_code_ref: CodeRef {
range: sketch_on_face_source_range,
node_path: NodePath::from_range(ast, sketch_on_face_source_range).unwrap_or_default(),
path_to_node: Vec::new(),
},
face_code_ref: sketch_on_face_code_ref,
cmd_id: artifact_command.cmd_id,
}));
let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {

View File

@ -1155,23 +1155,24 @@ impl ExecutorContext {
#[cfg(feature = "artifact-graph")]
{
// Move the artifact commands and responses to simplify cache management
// and error creation.
exec_state
.global
.artifact_commands
.extend(self.engine.take_artifact_commands().await);
exec_state
.global
.artifact_responses
.extend(self.engine.take_responses().await);
let new_commands = self.engine.take_artifact_commands().await;
let new_responses = self.engine.take_responses().await;
let initial_graph = exec_state.global.artifact_graph.clone();
// Build the artifact graph.
match build_artifact_graph(
&exec_state.global.artifact_commands,
&exec_state.global.artifact_responses,
let graph_result = build_artifact_graph(
&new_commands,
&new_responses,
program,
&exec_state.global.artifacts,
) {
&mut exec_state.global.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
exec_state.global.artifact_commands.extend(new_commands);
exec_state.global.artifact_responses.extend(new_responses);
match graph_result {
Ok(artifact_graph) => {
exec_state.global.artifact_graph = artifact_graph;
exec_result.map(|(_, env_ref, _)| env_ref)