Skip to content

Commit 01a2bbd

Browse files
authored
perf(compile): reuse rule results on warm and one-edit compiles (#915)
* perf(compile): reuse custom-rule results whose recorded fact reads are unchanged The isolated custom-rule host now records which compiler facts each rule invocation reads through its context. Results are cached per rule and subject and reused only while every recorded fact digest, the rule implementation and import closure, the rules configuration, the installed code identity, and the dialect are unchanged. Rules whose reachable module state changes between invocations, or that touch private view state, are treated as reading every fact. Custom rules no longer serialise the whole compiled project for a cache key: the project is pickled only when a host run is needed, and built-in rules key their project-level identity on request facts instead of walking project files. Refs CHI-443, CHI-444 * perf(compile): reuse the built-in rules response for an identical native request Warm compiles re-sent an unchanged 60 MB request to the native engine, which spent about a second re-validating per-model cache entries. The complete response is now memoized under a digest of the serialized request and the installed code identity, so any fact, scope, config or code change re-evaluates through the native per-model cache. Refs CHI-445 * perf(compile): resolve macro context injection once per loaded macro Every macro call rebuilt the callable's inspect signature to decide whether to inject the macro context. The decision is now a cached property of the loaded macro, removing about 3,600 signature constructions from a dense 3,000-model compile. Refs CHI-442 * perf(compile): guard dense warm and one-edit compile budgets and oracles Adds 3,000- and 5,000-model dense guards that compile cold, warm, and after a single model edit, then compare the edit with a from-scratch uncached oracle. They assert exact fingerprints, complete warm cache reuse, and that the edit re-evaluates only the edited model's built-in rules while the project-wide custom rule is reused. The dense job timeout grows to cover the extra compiles. Refs CHI-441 * fix(rules): verify filesystem reads and conservatively key untracked custom-rule results - Record the digest of each project-tree value a rule actually read in the host and refuse to store a result whose filesystem facts changed before the parent recorded the fingerprint. - Key untracked rules on their recorded reads plus every observable fact, so files outside the discovered tree (hidden directories) still invalidate them. - Never cache rules that touch private or unknown fact-view attributes. - Include the thread id in the custom-rule cache temporary file name.
1 parent afc2106 commit 01a2bbd

46 files changed

Lines changed: 3013 additions & 1056 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/verify.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ jobs:
365365
needs: [changes, lockfile]
366366
if: needs.changes.outputs.performance == 'true'
367367
runs-on: ubuntu-24.04
368-
timeout-minutes: 15
368+
timeout-minutes: 20
369369
strategy:
370370
fail-fast: false
371371
matrix:

‎crates/sqlbuild-rules-native/src/bindings/_helpers/functions.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ fn evaluate_json(py: Python<'_>, request_json: &str) -> PyResult<String> {
7373
.map_err(value_error)
7474
}
7575

76+
#[pyfunction]
77+
fn run_custom_host_json(py: Python<'_>, spec_json: &str) -> PyResult<String> {
78+
py.compiler_detach(|| crate::engine::main::custom_host::run_custom_host_json(spec_json))
79+
.map_err(value_error)
80+
}
81+
7682
#[pyfunction]
7783
fn lint_sql_json(py: Python<'_>, request_json: &str) -> PyResult<String> {
7884
py.compiler_detach(|| crate::sql_lint::main::engine::lint_json(request_json))
@@ -422,6 +428,7 @@ fn skill_freshness(content: Option<&str>, input_fingerprint: &str) -> String {
422428
pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
423429
module.add_function(wrap_pyfunction!(evaluate_json, module)?)?;
424430
module.add_function(wrap_pyfunction!(finalize_rule_findings_json, module)?)?;
431+
module.add_function(wrap_pyfunction!(run_custom_host_json, module)?)?;
425432
module.add_function(wrap_pyfunction!(lint_sql_json, module)?)?;
426433
module.add_function(wrap_pyfunction!(prepare_lint_sql, module)?)?;
427434
module.add(

‎crates/sqlbuild-rules-native/src/constants.rs‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ pub(crate) const GENERIC_BASIC_NAME: &str = "basic";
1313
pub(crate) const GENERIC_SCENARIO_NAME: &str = "scenario";
1414
pub(crate) const GENERIC_CASE_NAME: &str = "case";
1515
pub(crate) const CUSTOM_RULE_NAMESPACE: &str = "XSQBR";
16-
pub(crate) const TARGET_DIRECTORY: &str = "target";
1716
pub(crate) const REFERENCE_KIND: &str = "ref";
1817
pub(crate) const MODEL_SCHEMA_CONFIG_KEY: &str = "schema";
1918
pub(crate) const STAGING_LAYER_DIRECTORY: &str = "staging";
@@ -33,11 +32,6 @@ pub(crate) const NEGATION_OPERATOR: &str = "-";
3332
pub(crate) const DECLARATION_DOMAIN_COMPONENTS: usize = 3;
3433
pub(crate) const DIRECT_DEPENDENCY_PATH_LENGTH: usize = 2;
3534
pub(crate) const RULE_CODE_NUMBER_LENGTH: usize = 3;
36-
pub(crate) const CUSTOM_HOST_REQUIRED_ERROR: &str = "selected custom rules require a custom host";
37-
pub(crate) const LOGS_DIRECTORY: &str = "logs";
38-
pub(crate) const GIT_DIRECTORY: &str = ".git";
39-
pub(crate) const RULES_DIRECTORY: &str = "rules";
40-
pub(crate) const PYTHON_EXTENSION: &str = "py";
4135
pub(crate) const SQL_WILDCARD: &str = "*";
4236
pub(crate) const UNKNOWN_SQL_TYPE: &str = "UNKNOWN";
4337
pub(crate) const VARCHAR_SQL_TYPE: &str = "VARCHAR";

‎crates/sqlbuild-rules-native/src/engine/_helpers/cache.rs‎

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
use crate::models::Fault;
2-
use fensu_policy::lifecycle::models::CacheRead;
3-
use fensu_policy::{read_cache, write_cache};
42
use serde::{Deserialize, Serialize};
53
use std::collections::BTreeMap;
64
use std::path::{Path, PathBuf};
@@ -28,36 +26,6 @@ impl Cache {
2826
})
2927
}
3028

31-
pub(crate) fn get(&self, path: &str, fingerprint: &str) -> Result<Option<Vec<Fault>>, String> {
32-
match read_cache::<Vec<Fault>>(&self.root, path, fingerprint)
33-
.map_err(|error| format!("could not read rules cache: {error}"))?
34-
{
35-
CacheRead::Hit(faults) => Ok(Some(faults)),
36-
CacheRead::Miss | CacheRead::Invalidated => Ok(None),
37-
}
38-
}
39-
40-
pub(crate) fn put(
41-
&self,
42-
path: &str,
43-
fingerprint: &str,
44-
faults: &[Fault],
45-
) -> Result<(), String> {
46-
write_cache(&self.root, path, fingerprint, &faults)
47-
.map_err(|error| format!("could not write rules cache: {error}"))
48-
}
49-
50-
pub(crate) fn custom_rule_bucket(
51-
&self,
52-
code: &str,
53-
rule_identity: &str,
54-
) -> Result<RuleCacheBucket, String> {
55-
read_bucket(
56-
&self.root.join("bulk").join(format!("custom-{code}.json")),
57-
rule_identity,
58-
)
59-
}
60-
6129
pub(crate) fn native_rules_bucket(
6230
&self,
6331
ruleset_identity: &str,
@@ -76,19 +44,6 @@ impl Cache {
7644
bucket,
7745
)
7846
}
79-
80-
pub(crate) fn put_custom_rule_bucket(
81-
&self,
82-
code: &str,
83-
rule_identity: &str,
84-
bucket: &RuleCacheBucket,
85-
) -> Result<(), String> {
86-
write_bucket(
87-
&self.root.join("bulk").join(format!("custom-{code}.json")),
88-
rule_identity,
89-
bucket,
90-
)
91-
}
9247
}
9348

9449
fn read_bucket(path: &Path, fingerprint: &str) -> Result<RuleCacheBucket, String> {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::models::CustomHostSpec;
2+
use fensu_policy::lifecycle::models::{
3+
CustomHostInvocation, CustomHostOutputLimits, CustomHostRequest,
4+
};
5+
use fensu_policy::run_custom_host;
6+
use std::path::Path;
7+
use std::time::Duration;
8+
9+
pub(crate) fn run_custom_host_json(spec_json: &str) -> Result<String, String> {
10+
let spec: CustomHostSpec = serde_json::from_str(spec_json)
11+
.map_err(|error| format!("invalid custom host request: {error}"))?;
12+
let request = CustomHostRequest {
13+
protocol: fensu_policy::lifecycle::constants::CUSTOM_HOST_PROTOCOL_VERSION,
14+
runtime_version: spec.runtime_version.clone(),
15+
payload: spec.payload,
16+
};
17+
let response = run_custom_host::<_, serde_json::Value>(CustomHostInvocation {
18+
program: Path::new(&spec.program),
19+
arguments: &spec.arguments,
20+
timeout: Duration::from_millis(spec.timeout_millis),
21+
output_limits: CustomHostOutputLimits::default(),
22+
request: &request,
23+
})
24+
.map_err(|error| format!("invalid rules lifecycle: {error}"))?;
25+
let payload = response.payload.ok_or_else(|| {
26+
"custom rule host returned no payload after successful validation".to_owned()
27+
})?;
28+
serde_json::to_string(&payload).map_err(|error| error.to_string())
29+
}

0 commit comments

Comments
 (0)