perf(template-engine): read-only staleness/comparison paths skip Expression-trigger compiles — deploy gate remains the authoritative check (plan R2-05 T4)
This commit is contained in:
@@ -97,11 +97,16 @@ public class ValidationService
|
|||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="validateScriptCompilation">
|
/// <param name="validateScriptCompilation">
|
||||||
/// <c>true</c> (default) runs the authoritative Roslyn <see cref="ValidateScriptCompilation"/>
|
/// <c>true</c> (default) runs the authoritative Roslyn <see cref="ValidateScriptCompilation"/>
|
||||||
/// stage — the single most expensive check (a non-collectible assembly load per script).
|
/// stage — the single most expensive check (a non-collectible assembly load per script) —
|
||||||
/// <c>false</c> skips ONLY that stage; every other structural/semantic check still runs.
|
/// AND the Expression-trigger syntax/compile check
|
||||||
/// Read-only staleness/comparison callers (DeploymentManager's FlatteningPipeline on its
|
/// (<see cref="CheckExpressionSyntax"/>, a forbidden-API verdict plus a real compile of every
|
||||||
/// comparison/probe paths) pass <c>false</c> because they need only the flattened config and
|
/// Expression-triggered script/alarm body against <see cref="TriggerCompileSurface"/>).
|
||||||
/// revision hash, not a compile. The deploy gate keeps the default (<c>true</c>).
|
/// <c>false</c> skips ONLY those two compile stages; every other structural/semantic check
|
||||||
|
/// still runs — including, for Expression triggers, the blank-expression check and the
|
||||||
|
/// cheap attribute-reference scan. Read-only staleness/comparison callers (DeploymentManager's
|
||||||
|
/// FlatteningPipeline on its comparison/probe paths) pass <c>false</c> because they need only
|
||||||
|
/// the flattened config and revision hash, not a compile. The deploy gate keeps the default
|
||||||
|
/// (<c>true</c>) — it is the authoritative check for both stages.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <returns>A merged <see cref="ValidationResult"/> aggregating all pipeline stage outcomes.</returns>
|
/// <returns>A merged <see cref="ValidationResult"/> aggregating all pipeline stage outcomes.</returns>
|
||||||
public ValidationResult Validate(
|
public ValidationResult Validate(
|
||||||
@@ -128,7 +133,7 @@ public class ValidationService
|
|||||||
validateScriptCompilation ? ValidateScriptCompilation(configuration) : ValidationResult.Success(),
|
validateScriptCompilation ? ValidateScriptCompilation(configuration) : ValidationResult.Success(),
|
||||||
ValidateAlarmTriggerReferences(configuration),
|
ValidateAlarmTriggerReferences(configuration),
|
||||||
ValidateScriptTriggerReferences(configuration),
|
ValidateScriptTriggerReferences(configuration),
|
||||||
ValidateExpressionTriggers(configuration),
|
ValidateExpressionTriggers(configuration, validateScriptCompilation),
|
||||||
ValidateConnectionBindingCompleteness(configuration, enforceConnectionBindings, siteConnectionNames),
|
ValidateConnectionBindingCompleteness(configuration, enforceConnectionBindings, siteConnectionNames),
|
||||||
ValidateSchemaReferences(configuration, sharedScripts, resolveSchemaRef),
|
ValidateSchemaReferences(configuration, sharedScripts, resolveSchemaRef),
|
||||||
_semanticValidator.Validate(configuration, sharedScripts, alarmCapableConnectionNames)
|
_semanticValidator.Validate(configuration, sharedScripts, alarmCapableConnectionNames)
|
||||||
@@ -366,8 +371,16 @@ public class ValidationService
|
|||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||||
|
/// <param name="validateExpressionSyntax">
|
||||||
|
/// <c>true</c> (default) runs the authoritative <see cref="CheckExpressionSyntax"/>
|
||||||
|
/// forbidden-API + compile stage for each Expression trigger. <c>false</c> skips ONLY
|
||||||
|
/// that stage — the blank-expression check and the attribute-reference scan still run —
|
||||||
|
/// so read-only staleness/comparison callers do not pay the per-expression compile.
|
||||||
|
/// </param>
|
||||||
/// <returns>A <see cref="ValidationResult"/> with errors and warnings from all expression trigger checks.</returns>
|
/// <returns>A <see cref="ValidationResult"/> with errors and warnings from all expression trigger checks.</returns>
|
||||||
public static ValidationResult ValidateExpressionTriggers(FlattenedConfiguration configuration)
|
public static ValidationResult ValidateExpressionTriggers(
|
||||||
|
FlattenedConfiguration configuration,
|
||||||
|
bool validateExpressionSyntax = true)
|
||||||
{
|
{
|
||||||
var errors = new List<ValidationEntry>();
|
var errors = new List<ValidationEntry>();
|
||||||
var warnings = new List<ValidationEntry>();
|
var warnings = new List<ValidationEntry>();
|
||||||
@@ -382,7 +395,7 @@ public class ValidationService
|
|||||||
CheckExpressionTrigger(
|
CheckExpressionTrigger(
|
||||||
ValidationCategory.ScriptTriggerReference, "script",
|
ValidationCategory.ScriptTriggerReference, "script",
|
||||||
script.CanonicalName, script.TriggerConfiguration,
|
script.CanonicalName, script.TriggerConfiguration,
|
||||||
attributeNames, errors, warnings);
|
attributeNames, errors, warnings, validateExpressionSyntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var alarm in configuration.Alarms)
|
foreach (var alarm in configuration.Alarms)
|
||||||
@@ -393,7 +406,7 @@ public class ValidationService
|
|||||||
CheckExpressionTrigger(
|
CheckExpressionTrigger(
|
||||||
ValidationCategory.AlarmTriggerReference, "alarm",
|
ValidationCategory.AlarmTriggerReference, "alarm",
|
||||||
alarm.CanonicalName, alarm.TriggerConfiguration,
|
alarm.CanonicalName, alarm.TriggerConfiguration,
|
||||||
attributeNames, errors, warnings);
|
attributeNames, errors, warnings, validateExpressionSyntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ValidationResult { Errors = errors, Warnings = warnings };
|
return new ValidationResult { Errors = errors, Warnings = warnings };
|
||||||
@@ -424,7 +437,8 @@ public class ValidationService
|
|||||||
string? triggerConfigJson,
|
string? triggerConfigJson,
|
||||||
HashSet<string> attributeNames,
|
HashSet<string> attributeNames,
|
||||||
List<ValidationEntry> errors,
|
List<ValidationEntry> errors,
|
||||||
List<ValidationEntry> warnings)
|
List<ValidationEntry> warnings,
|
||||||
|
bool validateExpressionSyntax = true)
|
||||||
{
|
{
|
||||||
var expression = ExtractExpressionFromTriggerConfig(triggerConfigJson);
|
var expression = ExtractExpressionFromTriggerConfig(triggerConfigJson);
|
||||||
var strict = IsStrictAnalysis(triggerConfigJson);
|
var strict = IsStrictAnalysis(triggerConfigJson);
|
||||||
@@ -443,12 +457,20 @@ public class ValidationService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var syntaxError = CheckExpressionSyntax(expression);
|
// The syntax/compile stage is the expensive one (a forbidden-API verdict + a
|
||||||
if (syntaxError != null)
|
// real Roslyn compile of the expression). Read-only staleness/comparison
|
||||||
|
// callers pass validateExpressionSyntax: false to skip ONLY this stage; the
|
||||||
|
// blank-expression check above and the attribute-reference scan below still run.
|
||||||
|
// The deploy gate keeps the default (true) — the authoritative check.
|
||||||
|
if (validateExpressionSyntax)
|
||||||
{
|
{
|
||||||
errors.Add(ValidationEntry.Error(category,
|
var syntaxError = CheckExpressionSyntax(expression);
|
||||||
$"The {entityLabel} '{entityName}' expression trigger failed validation: {syntaxError}",
|
if (syntaxError != null)
|
||||||
entityName));
|
{
|
||||||
|
errors.Add(ValidationEntry.Error(category,
|
||||||
|
$"The {entityLabel} '{entityName}' expression trigger failed validation: {syntaxError}",
|
||||||
|
entityName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var attrName in ExtractAttributeReferences(expression))
|
foreach (var attrName in ExtractAttributeReferences(expression))
|
||||||
|
|||||||
@@ -462,4 +462,43 @@ public class ValidationServiceTests
|
|||||||
Assert.DoesNotContain(strictResult.Errors, e => e.Category == ValidationCategory.AlarmTriggerReference);
|
Assert.DoesNotContain(strictResult.Errors, e => e.Category == ValidationCategory.AlarmTriggerReference);
|
||||||
Assert.DoesNotContain(strictResult.Warnings, w => w.Category == ValidationCategory.AlarmTriggerReference);
|
Assert.DoesNotContain(strictResult.Warnings, w => w.Category == ValidationCategory.AlarmTriggerReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Validate_SkipCompilation_SkipsExpressionTriggerSyntaxCheck()
|
||||||
|
{
|
||||||
|
var config = ConfigWithExpressionTriggerScript("this is not C# ((");
|
||||||
|
var gated = new ValidationService().Validate(config, validateScriptCompilation: false);
|
||||||
|
Assert.DoesNotContain(gated.Errors, e => e.Message.Contains("failed validation")); // FAILS today
|
||||||
|
var full = new ValidationService().Validate(config);
|
||||||
|
Assert.Contains(full.Errors, e => e.Message.Contains("failed validation")); // deploy gate unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Validate_SkipCompilation_StillChecksExpressionAttributeReferences()
|
||||||
|
{
|
||||||
|
// expression "Attributes[\"Ghost\"] != null" referencing a missing attribute:
|
||||||
|
// the reference scan is cheap string work and MUST survive the gate.
|
||||||
|
var config = ConfigWithExpressionTriggerScript("Attributes[\"Ghost\"] != null");
|
||||||
|
var gated = new ValidationService().Validate(config, validateScriptCompilation: false);
|
||||||
|
Assert.Contains(gated.Errors, e => e.Message.Contains("Ghost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FlattenedConfiguration ConfigWithExpressionTriggerScript(string expression)
|
||||||
|
{
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(new { expression });
|
||||||
|
return new FlattenedConfiguration
|
||||||
|
{
|
||||||
|
InstanceUniqueName = "Instance1",
|
||||||
|
Attributes = [new ResolvedAttribute { CanonicalName = "Temp", Value = "25", DataType = "Double" }],
|
||||||
|
Scripts =
|
||||||
|
[
|
||||||
|
new ResolvedScript
|
||||||
|
{
|
||||||
|
CanonicalName = "OnExpr",
|
||||||
|
TriggerType = "Expression",
|
||||||
|
TriggerConfiguration = json
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user