Skip to content

Fix execution caching to cache artifact graph NodePath #6978

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions rust/kcl-lib/e2e/executor/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,18 +259,23 @@ extrude(profile001, length = 100)"#
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_add_line_preserves_artifact_commands() {
let code = r#"sketch001 = startSketchOn(XY)
|> startProfile(at = [5.5229, 5.25217])
|> line(end = [10.50433, -1.19122])
|> line(end = [8.01362, -5.48731])
|> line(end = [-1.02877, -6.76825])
|> line(end = [-11.53311, 2.81559])
profile001 = startProfile(sketch001, at = [5.5, 5.25])
|> line(end = [10.5, -1.19])
|> line(end = [8, -5.5])
|> line(end = [-1.02, -6.76])
|> line(end = [-11.5, 2.8])
|> close()
plane001 = offsetPlane(XY, offset = 20)
Comment on lines +262 to +268
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did the input kcl need to change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh

The sim test output is from fixing SketchOnFace and SketchOnPlane, just something I noticed was broken while investigating this.

never mind

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I used offsetPlane() was because I wanted to have something to refer to in the second execution, like in way we reproduced #6841.

The coordinate changes were arbitrary. Just wanted rounder numbers to look at.

"#;
// Use a new statement; don't extend the prior pipeline. This allows us to
// detect a prefix.
let code_with_extrude = code.to_owned()
+ r#"
extrude(sketch001, length = 4)
profile002 = startProfile(plane001, at = [0, 0])
|> line(end = [0, 10])
|> line(end = [10, 0])
|> close()
extrude001 = extrude(profile001, length = 4)
"#;

let result = cache_test(
Expand Down Expand Up @@ -305,6 +310,24 @@ extrude(sketch001, length = 4)
first.artifact_graph.len(),
second.artifact_graph.len()
);
// Make sure we have NodePaths referring to the old code.
let graph = &second.artifact_graph;
assert!(!graph.is_empty());
for artifact in graph.values() {
assert!(!artifact.code_ref().map(|c| c.node_path.is_empty()).unwrap_or(false));
assert!(
!artifact
.face_code_ref()
// TODO: This fails, but it shouldn't.
// .map(|c| c.node_path.is_empty())
// Allowing the NodePath to be empty if the SourceRange is [0,
// 0] as a more lenient check.
.map(|c| !c.range.is_synthetic() && c.node_path.is_empty())
.unwrap_or(false),
"artifact={:?}",
artifact
);
}
}

#[tokio::test(flavor = "multi_thread")]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 87 additions & 28 deletions rust/kcl-lib/src/execution/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -396,7 +396,6 @@ pub enum Artifact {
Cap(Cap),
SweepEdge(SweepEdge),
EdgeCut(EdgeCut),
#[expect(unused)]
EdgeCutEdge(EdgeCutEdge),
Helix(Helix),
}
Expand Down Expand Up @@ -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),
Expand All @@ -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> {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -1155,27 +1214,27 @@ 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,
sub_type,
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 {
Expand Down
31 changes: 16 additions & 15 deletions rust/kcl-lib/src/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ flowchart LR
1["Plane<br>[12, 29, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
2["StartSketchOnFace<br>[343, 382, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
16["Sweep Extrusion<br>[258, 290, 0]"]
%% [ProgramBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
17["Sweep Extrusion<br>[553, 583, 0]"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ flowchart LR
3["Plane<br>[110, 138, 0]"]
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit]
4["StartSketchOnPlane<br>[152, 181, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 3 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
1 <--x 4
1 --- 5
5 --- 6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ flowchart LR
1["Plane<br>[12, 29, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
2["StartSketchOnFace<br>[255, 294, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
3["StartSketchOnFace<br>[511, 550, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 4 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
4["StartSketchOnFace<br>[780, 819, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 6 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
29["Sweep Extrusion<br>[212, 242, 0]"]
%% [ProgramBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit]
30["Sweep Extrusion<br>[468, 498, 0]"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ flowchart LR
2["Plane<br>[1424, 1442, 0]"]
%% [ProgramBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit]
3["StartSketchOnFace<br>[309, 348, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 3 }, VariableDeclarationDeclaration, VariableDeclarationInit]
60["Sweep Extrusion<br>[264, 296, 0]"]
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit]
61["Sweep RevolveAboutEdge<br>[1300, 1366, 0]"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ flowchart LR
1["Plane<br>[6, 23, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
2["StartSketchOnFace<br>[203, 241, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
15["Sweep Extrusion<br>[169, 189, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 6 }]
16[Wall]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ flowchart LR
4["Plane<br>[1594, 1632, 0]"]
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }, CallKwUnlabeledArg]
5["StartSketchOnPlane<br>[1580, 1633, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
6["StartSketchOnPlane<br>[1580, 1633, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
7["StartSketchOnPlane<br>[1580, 1633, 0]"]
%% Missing NodePath
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
33["Sweep Loft<br>[3201, 3268, 0]"]
%% [ProgramBodyItem { index: 2 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 15 }, VariableDeclarationDeclaration, VariableDeclarationInit]
34[Wall]
Expand Down
Loading
Loading