Skip to content

perf!: replace merged data model YAML with JSON for faster loading - #933

Open
oboehmer wants to merge 7 commits into
mainfrom
feat/931-merged-data-model-json
Open

perf!: replace merged data model YAML with JSON for faster loading#933
oboehmer wants to merge 7 commits into
mainfrom
feat/931-merged-data-model-json

Conversation

@oboehmer

@oboehmer oboehmer commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Description

Replaces the internal merged data model file format from YAML to JSON. The merged data model is a temporary file passed between the nac-test orchestrator and test subprocesses via the MERGED_DATA_MODEL_TEST_VARIABLES_FILEPATH environment variable. Switching from ruamel's pure-Python YAML parser to json.dump/json.load significantly reduces per-test setup time. In one observation on a 2.3 MB file, data model load time dropped from ~1.9s to ~0.017s per test subprocess.

Closes

Related Issue(s)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring / Technical debt (internal improvements with no user-facing changes)
  • Documentation update
  • Chore (build process, CI, tooling, dependencies)
  • Other (please describe):

Test Framework Affected

  • PyATS
  • Robot Framework
  • Both
  • N/A (not test-framework specific)

Network as Code (NaC) Architecture Affected

  • ACI (APIC)
  • NDO (Nexus Dashboard Orchestrator)
  • NDFC / VXLAN-EVPN (Nexus Dashboard Fabric Controller)
  • Catalyst SD-WAN (SDWAN Manager / vManage)
  • Catalyst Center (DNA Center)
  • ISE (Identity Services Engine)
  • FMC (Firepower Management Center)
  • Meraki (Cloud-managed)
  • NX-OS (Nexus Direct-to-Device)
  • IOS-XE (Direct-to-Device)
  • IOS-XR (Direct-to-Device)
  • Hyperfabric
  • All architectures
  • N/A (architecture-agnostic)

Platform Tested

  • macOS (version tested: 15)
  • Linux (distro/version tested: )

Key Changes

  • nac_test/core/constants.py: MERGED_DATA_FILENAME value changed from .yaml to .json
  • nac_test/data_merger.py: write_merged_data_model() now uses json.dump() instead of nac_yaml.write_yaml_file()
  • nac_test/pyats_core/common/base_test.py: load_data_model() now uses json.load(), safe_load import removed
  • Tests, fixtures, and documentation updated throughout

Testing Done

  • Unit tests added/updated
  • Integration tests performed
  • Manual testing performed:
    • PyATS tests executed successfully
    • Robot Framework tests executed successfully
    • D2D/SSH tests executed successfully (if applicable)
    • HTML reports generated correctly
  • All existing tests pass (pytest / pre-commit run -a)

Test Commands Used

pytest tests/unit/ tests/integration/ -n auto --dist loadscope -q \
  --ignore=tests/integration/test_integration.py \
  --ignore=tests/integration/test_integration_robot_pabot.py
# 1008 passed

Checklist

  • Code follows project style guidelines (pre-commit run -a passes)
  • Self-review of code completed
  • Code is commented where necessary (especially complex logic)
  • Documentation updated (if applicable)
  • No new warnings introduced
  • Changes work on both macOS and Linux
  • CHANGELOG.md updated (if applicable)

Screenshots (if applicable)

N/A

Additional Notes

This change is only breaking for consumers that read the merged data model file directly via MERGED_DATA_MODEL_TEST_VARIABLES_FILEPATH using a YAML parser. Those consumers need to switch to json.load(). The standard self.data_model API requires no changes.

JSON serialization caveats vs. the previous YAML format: non-string mapping keys (e.g. integer VLAN IDs used as keys) are coerced to strings, and values JSON can't represent natively (e.g. an unquoted YAML date parsed as datetime.date) are written in string form via json.dump(..., default=str). This only affects consumers reading the file directly; the self.data_model API for typical string-keyed models is unaffected. Quote such keys/values in your data files if you need them preserved verbatim.

PR #898 proposed a JSON sidecar cache approach to solve the same performance problem. This PR takes a simpler path: JSON as the sole format, no sidecar logic, no mtime checks, no atomic tmp writes.

Additional Changes in this PR (beyond the JSON migration)

Building on the JSON migration, this branch also includes:

  • New feature — NAC_TEST_DUMP_YAML_DATA_MODEL: optional env var to also write the merged data model as a YAML file alongside the JSON, for post-run inspection/debugging. Not auto-cleaned up; may contain sensitive values (documented in README + CHANGELOG). Covered by a unit test (JSON/YAML content parity) and a subprocess integration test (YAML persists after run).
  • Test cleanup / refactoring:
    • Corrected stale "YAML" references for the (now JSON) merged data model in docstrings.
    • Created merged-data-model test fixtures as JSON to match the real format.
    • Consolidated the duplicated PyATSTestDirs type and pyats_test_dirs fixture into the top-level tests/conftest.py.

Follow-up

@oboehmer oboehmer added tech-debt General technical debt requiring refactoring refactor Code refactoring without changing functionality performance Changes improving performance labels Sep 1, 2026
@aitestino

Copy link
Copy Markdown
Collaborator

Hey @oboehmer, thank you for raising this — the performance analysis is solid and the 1.9s → 0.017s per subprocess improvement is compelling, especially at scale (322s saved on a 169-test device).

One consideration before we go all-in on JSON: the merged data model file isn't purely a machine-to-machine artifact. Engineers troubleshooting test failures will open that file to understand what values the framework actually used to render their tests. YAML is significantly more readable for that — no braces, no mandatory quotes, clean indentation on deeply nested network config. 2.3MB of JSON is painful to visually inspect.

I think the right tradeoff is: JSON by default for speed, with an opt-in to also write a human-readable YAML copy for debugging. Something like NAC_TEST_DUMP_YAML_DATA_MODEL=1 — follows the existing env var pattern (NAC_TEST_BROKER_SOCKET, NAC_TEST_DEVICE_EXECUTE_TIMEOUT, etc.). CI stays fast by default, and an engineer debugging locally just exports the var before re-running.

This would also cleanly supersede PR #898's sidecar approach — same performance win, simpler implementation, and the human-readable format is available when you need it without forcing anyone to install a separate tool.

What do you think?

P.S. — This comment was drafted using voice-to-text via Claude Code. If the tone comes across as overly direct or terse, please know that's just how it tends to phrase things. No offense or criticism is intended — this is purely an objective technical review of the PR. Thanks for understanding! 🙂

@oboehmer

oboehmer commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the comment, @aitestino .. I initially discounted the serviceability aspect as we have removed the file anyway after the run, but a NAC_TEST_DUMP_YAML_DATA_MODEL with a clear warning that it may leave sensitive values on disk is a great compromise.

The merged data model is now written and read as JSON instead of YAML.
json.dump/json.load replaces ruamel's pure-Python YAML parser, which in
one observation reduced per-test data model load time from ~1.9s to
~0.017s on a 2.3 MB file.

MERGED_DATA_FILENAME constant value updated to .json; all consumers,
tests, fixtures, and documentation updated accordingly. result.yaml
fixture replaced with result.json.

Closes #931
…YAML

Optionally write the merged data model as a companion YAML file alongside
the JSON, gated by the NAC_TEST_DUMP_YAML_DATA_MODEL env var, for post-run
inspection/debugging. The YAML file is not registered for cleanup (persists
after the run) and may contain sensitive values, so it is written with 0o600
permissions and a warning is logged advising manual removal.

- add dump_yaml parameter to DataMerger.write_merged_data_model()
- wire NAC_TEST_DUMP_YAML_DATA_MODEL constant through the CLI
- unit test for JSON/YAML content parity
- subprocess integration test verifying the YAML persists after exit
- assert no YAML by default in existing render test
- document env var in README and CHANGELOG
The merged data model file has been JSON since 383c957 (perf!: replace
merged data model YAML with JSON). Update two stale docstrings that still
described it as YAML.
The merged data model file is JSON (parsed via json.load in base_test).
Update remaining test fixtures that created or referenced it as YAML so
they match the real format:
- rename dummy .yaml files to .json
- write JSON content instead of YAML
…st.py

The PyATSTestDirs NamedTuple and pyats_test_dirs fixture were duplicated
identically in tests/pyats_core/conftest.py and tests/unit/conftest.py.
Move both to the top-level tests/conftest.py so they are shared across the
whole suite, and repoint the seven importers accordingly.
json.dump now uses default=str so values ruamel's safe loader yields that
JSON cannot natively encode (e.g. datetime.date from an unquoted YAML date)
are stringified instead of raising TypeError and aborting the run at merge
time.

device_inventory discovery now reads the merged model with json.load
instead of yaml.safe_load, matching base_test and making the JSON format
contract explicit (it previously worked only because JSON is a YAML subset,
via the slow ruamel path this migration set out to eliminate).

Document the two JSON serialization differences versus the previous YAML
format in the CHANGELOG breaking-change entry: non-string mapping keys
(e.g. integer VLAN IDs used as keys) are coerced to strings, and
non-JSON-native values are written in string form. Add unit tests covering
both behaviors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Changes improving performance refactor Code refactoring without changing functionality tech-debt General technical debt requiring refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: slow data model loading - YAML re-parsed by every test subprocess (~1.9s per test)

2 participants