Profiling Attribute Rule Performance

272
0
07-08-2025 10:00 AM
HaydenWelch
MVP Regular Contributor
1 0 272

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:

  1. Iterate the provided featureclasses
  2. Iterate the featureclass rules
  3. Enable one rule at a time and triggering update rules for all features the amount of times specified in `iterations`
  4. Collect all rule feature per second values and print the agregate features per second
  5. Restore each featureclass rule state to its original state (disabled by user stays disabled)

 

Example Output:

HaydenWelch_0-1751994132964.png

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

Contributors
About the Author
Hello! My name is Hayden Welch. I work in the OSP fiber optic design industry and focus heavily on building tools and automations for designing large scale networks using arcpy and ArcGIS Pro