I frequently use the builtin Time() function to test rule performance during validation, but that's only one test case.
I decided to build a Python tool that will do this profiling for you, it will disable all rules but the one being tested and trigger a row update on all features in the provided datasource (this will obviously only work for rules that are set to trigger on Update). The final output for each tested feature will be an agregate features/second count for the ruleset.
Included are several helper classes:
AttributeRule
@dataclass
class AttributeRule:
_parent: str
batch: bool
checkParameters: dict[str, str]
creationTime: str
description: str
errorMessage: str
errorNumber: int
evaluationOrder: int
excludeFromClientEvaluation: bool
fieldName: str
id: int
isEnabled: bool
name: str
referencesExternalService: bool
requiredGeodatabaseClientVersion: str
scriptExpression: str
severity: int
subtypeCode: int
subtypeCodes: list[int]
tags: str
triggeringEvents: list[Event]
triggeringFields: list[str]
type: _RuleType
userEditable: bool
@property
def t(self) -> RuleType:
if self.type == 'esriARTCalculation':
return 'CALCULATION'
elif self.type == 'esriARTConstraint':
return 'CONSTRAINT'
elif self.type == 'esriARTValidation':
return 'VALIDATION'
raise ValueError(f'Invalid rule type: {self.type}, must be one of {_RuleType.__args__}')
def enable(self):
return EnableAttributeRules(self._parent, [self.name], self.t)
def disable(self):
return DisableAttributeRules(self._parent, [self.name], self.t)
A dataclass object that allows attribute rules to be unpacked from da.Describe and handles the enabling and disabling of the rule. `_parent` is bound to the object in the main loop and is just a path to the Featureclass datasource.
RuleProfiler
class RuleProfiler:
""" Profiles all rules in a featureclass """
def __init__(self) -> None:
self.description = "Profiles all rules in a featureclass"
self.label = "Profile Attribute Rules"
def getParameterInfo(self) -> list[Parameter]:
fcs = Parameter(
displayName="Feature Classes",
name="fcs",
datatype="GPFeatureLayer",
parameterType="Optional",
direction="Input",
multiValue=True,
)
iterations = Parameter(
displayName="Iterations",
name="iterations",
datatype="GPLong",
parameterType="Optional",
direction="Input",
)
iterations.filter.list = [1, 2, 3, 4, 5]
iterations.value = 2
target = Parameter(
displayName='Target Features per Second',
name='target',
datatype='GPLong',
parameterType='Required',
direction='Input',
)
target.value = 100
return [fcs, iterations, target]
def execute(self, parameters: list[Parameter], messages) -> None:
iterations = parameters[1].value
target = parameters[2].value
features_to_update: list[Layer] = parameters[0].values
if not features_to_update:
print("No feature classes selected.")
return
for feature_layer in features_to_update:
datasource = feature_layer.dataSource
feature_name = datasource.split('\\')[-1]
# Skip empty FCs
count = sum(1 for _ in SearchCursor(datasource, ['OID@']))
if count == 0:
continue
# Skip FCs with no rule
attribute_rules = [
AttributeRule(**{'_parent': datasource}, **r)
for r in Describe(datasource).get('attributeRules', [])
]
if not attribute_rules:
continue
print(f'Profiling {feature_name}:')
SetProgressorLabel(f"Profiling attribute rules for {feature_layer}...")
# Disable all rules
state = {}
for rule in attribute_rules:
state[rule.name] = rule.isEnabled
if rule.isEnabled:
rule.disable()
ResetProgressor()
SetProgressor("step", "Profiling attribute rules...", 0, len(attribute_rules), 1)
profile_results = []
for rule in attribute_rules:
try:
rule.enable()
start = time()
for iteration in range(1, iterations+1):
with Editor(feature_layer.connectionProperties['connection_info']['database']):
with UpdateCursor(datasource, ["OID@"]) as cursor:
for idx, row in enumerate(cursor):
SetProgressorLabel(
f"Profiling {rule.name} for {feature_layer} ({idx}/{count}) | (x{iteration})"
)
cursor.updateRow(row)
runtime = time() - start
rule.disable()
features_tested = count * iterations
features_per_second = features_tested / runtime
profile_results.append(features_per_second)
status = 'INFO'
if features_per_second < target:
status = 'WARNING'
if features_per_second < target/2:
status = 'ERROR'
print(f'\t{rule.name}: {features_per_second:0.2f} feats/sec', severity=status)
except Exception as e:
print(f'\tFailed to enable {rule.name}')
continue
SetProgressorPosition()
averave_features_per_second = sum(profile_results) / len(profile_results)
print('-'*50)
print(f'\tFeatures/Sec Agregate: {averave_features_per_second/len(attribute_rules):0.2f}')
print('')
# Restore state
for rule in attribute_rules:
rule_state = state.get(rule.name, None)
if rule_state is None:
continue
if rule_state:
rule.enable()
else:
rule.disable()
This is the entire toolcode that follows the process of:
- Iterate the provided featureclasses
- Iterate the featureclass rules
- Enable one rule at a time and triggering update rules for all features the amount of times specified in `iterations`
- Collect all rule feature per second values and print the agregate features per second
- Restore each featureclass rule state to its original state (disabled by user stays disabled)
Example Output:

Attached is the full PYT file (the forum won't allow pyt uploads, so just rename the py to pyt)
GitHub Repo: https://github.com/hwelch-fle/attribute_rule_profiler