Skip to content

[202405][GCU perf backport #3831] Config-threading refactor (part 1/N)#417

Open
rimunagala wants to merge 4 commits into
Azure:202405from
rimunagala:rimunagala/202405-gcu-3831-port
Open

[202405][GCU perf backport #3831] Config-threading refactor (part 1/N)#417
rimunagala wants to merge 4 commits into
Azure:202405from
rimunagala:rimunagala/202405-gcu-3831-port

Conversation

@rimunagala

@rimunagala rimunagala commented Jul 21, 2026

Copy link
Copy Markdown

Summary

Backports upstream sonic-utilities #3831 (GCU perf refactor) to 202405 in 4 parts:

  • Part 1/N — Config-threading refactor (fetch config once, thread through ChangeApplier)
  • Part 2/N — PathAddressing xpath cache + batch find_ref_paths
  • Part 3/N — patch_sorter: JsonMoveGroup + Bulk generators + validator refactor
  • Part 4/N — Fix-up (dangling caller, missing imports, test fixtures)

Diff vs Azure/sonic-utilities.msft:202412 for ported prod files:

  • change_applier.py, patch_sorter.py: 0 (identical)
  • gu_common.py: -45 (illegal_dataacl_check method excluded — from unrelated #3668)
  • generic_updater.py: -29 (skip_sort_tables + extract_scope multi-ASIC guard excluded — out of scope)

Requires coordinating buildimage-msft PR #2733 (3-layer YANG DFS enhancement).

Perf validation (str3-7800-lc3-1, single asic0 scope)

Mini-battery (3 iters × 3 scenarios: portchannel_add, mirror_session_add, acl_table_add):

  • Baseline stock 202405: ~4.2s per iter
  • With this PR: ~4.1s per iter (small patches — no meaningful delta)

Extended battery (13 scenarios × 5 iters):

  • Aggregate: 13.57× speedup vs stock 202405 on realistic multi-op configs
  • Best gains: scale_add_500 15.2×, real_apply_3 14.8×, op_replace_20 12.1×

400G bring-up (C_400g_config_noBufferPG.json): 58.5s (vs stock 109.7s), see caveat below.

Testing done

  • Unit tests: full generic_config_updater test suite passes
  • LC KVM smoke: sudo config apply-patch round-trips OK for portchannel/mirror/ACL/queue/buffer-profile
  • Multi-ASIC scope: verified asic0/asic1 patches route correctly via extract_scope
  • Negative cases (abc_C1..C4): correctly reject malformed JSON, invalid op, dangling ref, schema violation
  • Rollback path: config replace on real_rollback_3 succeeds
  • 100G bring-up: passes cleanly at 4.1s

Known caveat — 400G config-apply

400G port-add via config apply-patch fails with KeyError('PORTCHANNEL_MEMBER') in RemoveCreateOnlyDependencyMoveValidator._validate_member. Root cause is DFS visit order in patch_sorter.py on 400G's larger move set (8 lanes vs 100G's 2) — the validator inspects PORTCHANNEL_MEMBER before the parent table is populated.

Applying the natural fix (upstream #4668 try/except) makes the DFS backtrack instead of aborting, but on 400G the backtrack space is exponential — process hangs at 99% CPU (py-spy shows 50+ deep recursion, each node full-YANG re-parse via FullConfigMoveValidator). Full 5-PR chain (+#4118 +#26563 backport) has same DFS explosion.

100G/breakout/all other scenarios unaffected. 400G port additions must currently go through the native config interface breakout path (which does not use GCU) until the follow-up fix lands.

Follow-up: separate PR planned to address DFS-explosion class of bug (needs deeper upstream fix chain analysis).

Signed-off-by: Rithvick Reddy Munagala rimunagala@microsoft.com

…r: fetch config once per patch, thread through ChangeApplier

Backport of the config-threading portion of upstream sonic-net/sonic-utilities
PR #3831 to the 202405 branch, mirroring the 202412 auto-backport (PR Azure#254).

Motivation:
On 202405 the ChangeApplier.apply() path calls get_config_db_as_json()
twice per JsonChange (once to seed the deepcopy, once for the post-apply
sanity check). At MOR scale (N=14 => 381 changes) this dominates wall
clock time and yields the flat ~1.47s/change floor observed in the
sonic-mgmt scaling test.

This change:
  * JsonChange.apply(config) -> JsonChange.apply(config, in_place=False)
  * DryRunConfigWrapper.apply_change_to_config_db(change)
      -> apply_change_to_config_db(current_config_db, change) and returns
         the updated dict.
  * ChangeApplier.apply(change) -> apply(current_configdb, change) -> dict
    - drops the deepcopy (uses in_place=False on JsonChange.apply)
    - drops the redundant post-apply get_config_db_as_json + jsondiff
      sanity check; replaces it with a 1s sleep race-guard (matches
      upstream #3831 behavior; upstream SONiC issue tracks proper fix).
    - returns the updated in-memory config dict.
  * DryRunChangeApplier.apply likewise threads and returns config.
  * PatchApplier.apply now fetches old_config once, seeds current_config
    from it, and threads current_config = self.changeapplier.apply(
    current_config, change) through the change loop.

Tests updated to match new signatures:
  * change_applier_test.py: mock_obj.apply(config, in_place), applier
    call sites pass current_config, DryRunChangeApplier test updated.
  * multiasic_change_applier_test.py: @patch targets moved from
    change_applier.get_config_db_as_json to gu_common.get_config_db_as_json
    (since change_applier no longer re-exports it), applier call sites
    updated, `import copy` added.
  * generic_updater_test.py: TestPatchApplier now asserts
    changeapplier.apply.assert_called() and the mock side_effect
    accepts (current_config, change) and returns the config through.
  * gu_common_test.py: DryRunConfigWrapper multi-call test threads
    the returned config through the loop.

Deliberately out of scope for this commit (will land in follow-ups):
  * skip_sort_tables.txt handling (perpetuates a shortcut we want to
    eliminate on 202412; not required for perf win).
  * illegal_dataacl_check (comes from upstream PR #3668, not #3831).
  * PathAddressing xpath cache rewrite and find_ref_paths batch API.
  * BulkLowLevelMoveGenerator family (patch_sorter additive changes).
  * extract_scope multi-ASIC guard.

Reference: Azure/sonic-utilities.msft PR Azure#254 (202412 auto-backport of
upstream sonic-net/sonic-utilities #3831).

Signed-off-by: Rithvick Reddy Munagala <rimunagala@microsoft.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

…he + batch find_ref_paths

Backport of the gu_common.py PathAddressing portion of upstream
sonic-net/sonic-utilities PR #3831 to 202405, mirroring 202412
auto-backport (Azure/sonic-utilities.msft PR Azure#254).

Motivation:
On 202405, PathAddressing rebuilds its sonic-yang xpath token index
from scratch on every conversion call. At MOR scale (14 hosts *
27+ paths per patch = several hundred conversions per apply) this
produces the O(N) yang-tree-walk cost observed as the second-largest
contributor to GCU wall-clock time after the double get_config_db_as_json
addressed in part 1.

Also, find_ref_paths(path, ...) took a single path and had to reload
config for every call. The batched find_ref_paths(paths, ...) walks
config once per batch.

This change:
  * PathAddressing xpath cache rewrite: retire the ~500 lines of
    _get_xpath_tokens_from_* / _get_path_tokens_from_* walkers +
    _get_*_model / _find_leafref_paths helpers. Replace with a
    hash-keyed loadData cache so repeated conversions on the same
    config skip the yang reload. Public convert_path_to_xpath,
    convert_xpath_to_path, get_path_tokens, get_xpath_tokens,
    create_path, has_path, get_from_path, is_config_different
    signatures unchanged.
  * find_ref_paths(path, ...) -> find_ref_paths(paths, ...) batch
    API. Accepts either a single path (str) or a list of paths.
  * New helpers: configdb_sorted_keys_by_backlinks and configdb_sort_cmp
    used by patch_sorter's Bulk generators (added in part 3).
  * DryRunConfigWrapper.apply_change_to_config_db signature retained
    from part 1 (current_config_db threaded, returns updated dict).
  * JsonChange.apply(config, in_place=False) retained from part 1.

Test file wholesale-replaced from 202412 (adds caching-behavior tests
test_validate_config_db_config__same_config_called_twice__loadData_called_once,
test_find_ref_paths__after_validate_same_config__loadData_skipped, etc;
retires create_xpath / walker tests whose helpers are being deleted).

Deliberately excluded (not part of #3831):
  * ConfigWrapper.illegal_dataacl_check (from upstream PR #3668).

Reference: Azure/sonic-utilities.msft PR Azure#254 (202412 auto-backport of
upstream sonic-net/sonic-utilities #3831).

Signed-off-by: Rithvick Reddy Munagala <rimunagala@microsoft.com>
…oup + Bulk generators + validator refactor

Backport of the patch_sorter.py portion of upstream sonic-net/sonic-utilities
PR #3831 to 202405, mirroring 202412 auto-backport (Azure/sonic-utilities.msft PR Azure#254).

Motivation:
On 202405 the move-generation engine emits one JsonMove per individual
field/key delta, and each JsonMove goes through full-graph validation
independently. At MOR scale (14 hosts) the sorter emits several hundred
moves, and validators reload sonic-yang xpath state on every call.
Combined with the per-move DB roundtrip removed in part 1, this is the
second-largest contributor to observed GCU wall-clock time.

This change ports the sorter/validator rewrite:

New:
  * JsonMoveGroup - a mergeable group of JsonMoves. apply()/undo()
    fold a whole group into config in one pass, in_place-aware.
  * BulkLowLevelMoveGenerator, BulkKeyLevelMoveGenerator,
    BulkKeyGroupLowLevelMoveGenerator - emit JsonMoveGroups that
    coalesce many single-field/key deltas into one add/remove/replace
    move per subtree, with restricted-only fallback for correctness.
  * MoveWrapper.apply_move / undo_move now take in_place: bool.
  * Simulator.simulate / undo_simulate now take in_place: bool.

Refactor:
  * All *MoveValidator.validate signatures unified to
    (group: JsonMoveGroup, diff, simulated_config) - the simulated
    config is now computed once by the caller and passed in, instead
    of each validator recomputing it.
  * _extend_moves(moveGroup) and target_in_required_pattern helpers.
  * KeyLevelMoveGenerator / LowLevelMoveGenerator retained but now
    delegate to the Bulk variants under the hood via generate_groups
    with restricted_only fallback.
  * find_ref_paths now called with a paths batch and reload_config
    hint at PatchSorter._validate_paths_config; the reload skip is
    what unlocks the xpath cache added in part 2.

Retired:
  * SingleRunLowLevelMoveGenerator - internal-only, replaced by
    BulkLowLevelMoveGenerator's generate_remove/replace/add.

External API preserved:
  * StrictPatchSorter, NonStrictPatchSorter, PatchSorter,
    ConfigSplitter, TablesWithoutYangConfigSplitter,
    IgnorePathsFromYangConfigSplitter (the six symbols imported
    by generic_updater.py) - all signatures unchanged.

Test file wholesale-replaced from 202412 - covers the new Bulk
generators, JsonMoveGroup, and updated validator signatures.

Reference: Azure/sonic-utilities.msft PR Azure#254 (202412 auto-backport of
upstream sonic-net/sonic-utilities #3831).

Signed-off-by: Rithvick Reddy Munagala <rimunagala@microsoft.com>
…+ missing imports + missing test fixtures

Follow-up to parts 1-3 after cross-checking against PR Azure#254 file list.
No functional changes to the ported feature - only completes the port.

Fixes:

1. gu_common.py: remove call site of deleted illegal_dataacl_check
   Part 2 removed the illegal_dataacl_check method as out-of-scope
   (from upstream PR #3668, not #3831) but left the caller in
   validate_field_operation, which would AttributeError at runtime.

2. generic_updater.py: add missing 'import jsonpatch' and add
   'JsonChange' to the .gu_common import list. This is a pre-existing
   latent bug on 202405: PatchApplier.apply(patch, sort=False) uses
   JsonChange(jsonpatch.JsonPatch([element])) but neither symbol was
   imported. NameError only surfaces when the sort=False path is
   exercised. Upstream 202412 already imports both.

3. tests/generic_config_updater/gutest_helpers.py: add JsonMoveGroup
   import + create_side_effect_skiplastarg_dict,
   create_side_effect_skipfirstarg_dict,
   create_side_effect_jsonmovegroup_dict factories. These helpers
   are referenced by the 202412-shape patch_sorter_test.py added in
   part 3 - without them, patch_sorter_test collection fails.

4. tests/generic_config_updater/gcu_feature_patch_application_test.py:
   drop @patch('generic_config_updater.change_applier.get_config_db_as_json',
   ...) decorators. Part 1 dropped the get_config_db_as_json re-export
   from change_applier.py; mock decorators pointing at the removed
   attribute would AttributeError at test-setup time.

5. tests/generic_config_updater/files/patch_sorter_test_success.json:
   refresh golden fixture. The Bulk generators added in part 3 emit
   coalesced 'replace' moves on whole arrays where the old sorter
   emitted a sequence of per-element 'add' moves - the e2e success
   tests compare emitted output to this fixture.

6. tests/generic_config_updater/change_applier_test.py: drop stray
   local `running_config = {}` that shadowed the module-level global
   (functionally identical, but keeps parity with 202412 shape).

After these fixes, our branch's diff vs Azure/sonic-utilities.msft:202412
for the ported prod files is:
  change_applier.py: 0 (identical)
  patch_sorter.py:   0 (identical)
  gu_common.py:      -45 (illegal_dataacl_check method only)
  generic_updater.py: -29 (skip_sort_tables block + extract_scope
                          multi-ASIC guard + import fnmatch)
And for test files, only the tests exercising the three deliberately
excluded features differ.

Signed-off-by: Rithvick Reddy Munagala <rimunagala@microsoft.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@rimunagala
rimunagala force-pushed the rimunagala/202405-gcu-3831-port branch from 0acf307 to a205f2b Compare July 23, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant