Recheck-request: github-robot-_Build_and_Test


Rosemarie O'Riorden

Lowell, MA, United States

[email protected]

On Wed, Jul 22, 2026 at 8:24 PM Rosemarie O'Riorden via dev <
[email protected]> wrote:

> This commit adds a test that developers can run to check if the changes
> they've made cause a regression in the performance of ovn-controller or
> ovn-northd.
>
> Run from inside the sandbox and compare two commits' total script run
> time and peak memory usage for each process.
>
> This test creates a topology consisting of logical switches and routers,
> ACLs, port groups, NAT, DHCP, DNS, QoS, routing policies, and load
> balancers. The user can also provide their own database file for the
> benchmark.
>
> * To run the test with defaults (200 nodes, track northd and controller):
>     ./ovn-benchmark.sh
> * To run with 50 nodes: ./ovn-benchmark.sh 50
> * To run with 30 nodes and only track ovn-northd:
>     ./ovn-benchmark.sh 30 northd
> * To run with a custom db file: ./ovn-benchmark.sh -f file.db
> * To run with debug info printed: ./ovn-benchmark.sh --debug
> * To see detailed usage instructions: ./ovn-benchmark.sh --help
>
> Each "node" is a router-switch pair with associated features:
>  - 1 gateway router (with NAT, static routes, routing policies)
>  - 1 logical switch (with 9 ports by default, DHCP, DNS, QoS)
>  - 5 load balancers (each with 5 backends)
>  - ACLs, port groups, and address sets (shared across all nodes)
>
> For example, 200 nodes creates:
>  - 200 routers + 200 switches
>  - 1,800 logical switch ports (9 per switch)
>  - 1,000 load balancers (5 per node)
>  - 600 NAT rules, 600 static routes, 400 routing policies
>  - Plus ACLs, DHCP/DNS records, QoS rules, etc.
>
> ovn-benchmark.py creates the topology and ovn-benchmark.sh is the
> wrapper that tracks peak memory and execution time. ovn-benchmark.py was
> built off of ovn-lb-benchmark.py as a starting point.
>
> Reported-at: https://redhat.atlassian.net/browse/FDP-978
> Assisted-by: Claude Sonnet 4.5, Claude Code
> Signed-off-by: Rosemarie O'Riorden <[email protected]>
> ---
> v1 -> v2:
>  - Topology:
>    - Restructured from 1:1 chassis:node to a nodes-per-chassis style
>      where each chassis handles a partition or 'batch' of the network
> (ascii
>      diagram added).
>    - Added --batch-size (-b) for nodes-per-chassis grouping.
>    - Added IPv6 coverage.
>  - IP addressing:
>    - ip_node()/ip6_node() helpers spread index across two octets to support
>      >255 nodes.
>    - Added bounds checks for node count and ports-per-switch.
>  - Shell fixes:
>    - pgrep -x for exact process match.
>    - break 2 to exit both watcher loops on process death.
>    - trap cleanup EXIT with mktemp -d for temp files.
>    - Renamed SECONDS bc it's a bash keyword.
>    - python -> python3.
>    - Quoted array expansions, fixed PID error messages.
>  - Code cleanup:
>    - Extracted _add_node(), _add_ports(), _add_acl() helpers.
>    - Removed dead code (find_by_name(), seqno).
>    - Fixed error variable shadowing imported module 'error'.
>  - Docs:
>    - Added ovn-benchmark section to testing.rst.
>    - Added note in submitting-patches.rst.
> ---
>  .../contributing/submitting-patches.rst       |   4 +
>  Documentation/topics/testing.rst              |  32 +
>  tutorial/automake.mk                          |   4 +-
>  tutorial/ovn-benchmark.py                     | 836 ++++++++++++++++++
>  tutorial/ovn-benchmark.sh                     | 234 +++++
>  5 files changed, 1109 insertions(+), 1 deletion(-)
>  create mode 100644 tutorial/ovn-benchmark.py
>  create mode 100755 tutorial/ovn-benchmark.sh
>
> diff --git a/Documentation/internals/contributing/submitting-patches.rst
> b/Documentation/internals/contributing/submitting-patches.rst
> index beede72a2..6e3c4297c 100644
> --- a/Documentation/internals/contributing/submitting-patches.rst
> +++ b/Documentation/internals/contributing/submitting-patches.rst
> @@ -68,6 +68,10 @@ Testing is also important:
>    feature.  A bug fix patch should preferably add a test that would
>    fail if the bug recurs.
>
> +- To check for memory or performance regressions, you can run
> +  ``./ovn-benchmark.sh`` inside the OVN sandbox (``make sandbox``).
> +  See the "ovn-benchmark" section of :doc:`/topics/testing` for details.
> +
>  If you are using GitHub, then you may utilize the GitHub Actions CI
> system.
>  This will run the above tests automatically when you push changes to your
>  repository.
> diff --git a/Documentation/topics/testing.rst
> b/Documentation/topics/testing.rst
> index 579422ca0..bb7436b9a 100644
> --- a/Documentation/topics/testing.rst
> +++ b/Documentation/topics/testing.rst
> @@ -294,6 +294,38 @@ of these cached objects, be sure to rebuild the test.
>  The cached objects are stored under the relevant folder in
>  ``tests/perf-testsuite.dir/cached``.
>
> +ovn-benchmark
> ++++++++++++++
> +
> +The ``tutorial/`` directory contains a memory and performance benchmarking
> +tool that can be used to detect regressions between commits:
> +
> +- ``ovn-benchmark.sh``: Shell wrapper that tracks peak memory (RSS) and
> +  wall-clock time for ``ovn-northd`` and ``ovn-controller``.
> +- ``ovn-benchmark.py``: Python script that populates the Northbound
> database
> +  with a realistic ovn-kubernetes-style topology.
> +
> +The generated topology includes gateway routers, logical switches, NAT
> rules,
> +ACLs, DHCP/DHCPv6, DNS, load balancers, QoS rules, address sets, port
> groups,
> +static routes, and routing policies -- all with both IPv4 and IPv6
> coverage.
> +
> +The benchmark must be run from inside the OVN sandbox.  Run
> +``./ovn-benchmark.sh --help`` for the full list of options::
> +
> +    $ make sandbox
> +    $ ./ovn-benchmark.sh
> +
> +By default, the topology is generated through many individual OVSDB
> +transactions via ``ovn-benchmark.py``.
> +
> +.. note::
> +
> +   The ``-f`` option loads a Northbound database file via ``ovsdb-client
> +   restore`` instead, which applies the entire database as a single
> +   transaction.  This may yield different memory and timing results
> because
> +   ``ovn-northd`` and ``ovn-controller`` process one large batch of
> changes
> +   rather than reacting to each transaction incrementally.
> +
>  OVN Upgrade Testing
>  ~~~~~~~~~~~~~~~~~~~
>
> diff --git a/tutorial/automake.mk b/tutorial/automake.mk
> index 631208639..fdea2449c 100644
> --- a/tutorial/automake.mk
> +++ b/tutorial/automake.mk
> @@ -2,7 +2,9 @@ EXTRA_DIST += \
>         tutorial/ovn-sandbox \
>         tutorial/ovn-setup.sh \
>         tutorial/ovn-lb-benchmark.sh \
> -       tutorial/ovn-lb-benchmark.py
> +       tutorial/ovn-lb-benchmark.py \
> +       tutorial/ovn-benchmark.sh \
> +       tutorial/ovn-benchmark.py
>  sandbox: all
>         cd $(srcdir)/tutorial && MAKE=$(MAKE) HAVE_OPENSSL=$(HAVE_OPENSSL)
> \
>                 ./ovn-sandbox -b $(abs_builddir) --ovs-src $(ovs_srcdir)
> --ovs-build $(ovs_builddir) $(SANDBOXFLAGS)
> diff --git a/tutorial/ovn-benchmark.py b/tutorial/ovn-benchmark.py
> new file mode 100644
> index 000000000..1ecf0b5b4
> --- /dev/null
> +++ b/tutorial/ovn-benchmark.py
> @@ -0,0 +1,836 @@
> +#!/usr/bin/env python3
> +"""OVN memory regression testing tool.
> +
> +Creates a broad OVN topology to detect memory regressions between commits.
> +Designed to be run via ovn-benchmark.sh. (Run ./ovn-benchmark.sh --help
> +to see usage).
> +
> +Topology (simulates the default ovn-kubernetes topology):
> +
> +         lsp-0-*            lsp-1-*          (Workload ports)
> +            |                  |
> +         ls-0               ls-1             (Logical Switches)
> +            |                  |
> +         s2c-0/c2s-0        s2c-1/c2s-1
> +              \\               /
> +         +--- cluster (LR) ---+              (Cluster Router)
> +                    |
> +                sjc/rcj
> +                    |
> +         +----  join (LS)  ----+              (Join Switch)
> +              /               \\
> +         j2lr-0/lr2j-0     j2lr-1/lr2j-1
> +            |                  |
> +         lr-0               lr-1             (Gateway Routers)
> +
> +  Chassis binding (with batch size B, default n/10):
> +    chassis-0: c2s-[0..B), lr-[0..B)
> +    chassis-1: c2s-[B..2B), lr-[B..2B)
> +    ...
> +
> +Each node creates a dual-stack gateway router + logical switch pair
> +with:
> +  - Dual-stack addressing (IPv4 10.x.x.x/24, IPv6 fd00:x::/64)
> +  - NAT (SNAT/DNAT/DNAT_AND_SNAT), static routes, routing policies
> +  - Configurable ports per switch with port security
> +  - Security: Address sets, port groups, ACLs
> +  - Services: DHCP, DHCPv6, DNS, load balancers
> +  - QoS: Bandwidth limiting, DSCP marking
> +
> +Note: Uses explicit (non-templated) load balancers to maximize memory
> +usage for regression testing. For templated LB testing, see
> +ovn-lb-benchmark.py.
> +"""
> +
> +import argparse
> +import sys
> +
> +import ovs.db.idl
> +import ovs.jsonrpc
> +import ovs.poller
> +import ovs.stream
> +import ovs.vlog
> +from ovs.db import error
> +
> +vlog = ovs.vlog.Vlog('ovn-benchmark')
> +vlog.set_levels_from_string('console:warn')
> +vlog.init(None)
> +
> +SCHEMA = '../ovn-nb.ovsschema'
> +
> +
> +def die(msg):
> +    sys.stderr.write(f'\nError: {msg}\n')
> +    sys.exit(1)
> +
> +
> +def ip_node(i):
> +    """Convert node index to two IP octets, supporting up to 65535
> nodes."""
> +    return f'{i >> 8}.{i & 0xff}'
> +
> +
> +def ip6_node(i):
> +    """Convert node index to an IPv6 hextet for ULA addresses."""
> +    return f'{i:x}'
> +
> +
> +def create_address_sets(idl, n):
> +    """Create address sets for security groups."""
> +    vlog.info('Creating address sets')
> +    txn = ovs.db.idl.Transaction(idl)
> +
> +    web_as = txn.insert(idl.tables['Address_Set'])
> +    web_as.name = 'web_servers'
> +    web_as.addresses = (
> +        [f'10.{ip_node(i)}.10' for i in range(n)]
> +        + [f'fd00:{ip6_node(i)}::a' for i in range(n)])
> +
> +    db_as = txn.insert(idl.tables['Address_Set'])
> +    db_as.name = 'db_servers'
> +    db_as.addresses = (
> +        [f'10.{ip_node(i)}.20' for i in range(n)]
> +        + [f'fd00:{ip6_node(i)}::14' for i in range(n)])
> +
> +    app_as = txn.insert(idl.tables['Address_Set'])
> +    app_as.name = 'app_servers'
> +    app_as.addresses = (
> +        [f'10.{ip_node(i)}.30' for i in range(n)]
> +        + [f'fd00:{ip6_node(i)}::1e' for i in range(n)])
> +
> +    trusted_as = txn.insert(idl.tables['Address_Set'])
> +    trusted_as.name = 'trusted_networks'
> +    trusted_as.addresses = ['192.168.0.0/16', '172.16.0.0/12',
> +                            'fc00::/7']
> +
> +    if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +        die(f'Failed to create address sets ({txn.get_error()})')
> +
> +
> +def create_port_groups(idl):
> +    """Create port groups for security group implementation."""
> +    vlog.info('Creating port groups')
> +    txn = ovs.db.idl.Transaction(idl)
> +
> +    web_pg = txn.insert(idl.tables['Port_Group'])
> +    web_pg.name = 'web_tier'
> +
> +    db_pg = txn.insert(idl.tables['Port_Group'])
> +    db_pg.name = 'db_tier'
> +
> +    app_pg = txn.insert(idl.tables['Port_Group'])
> +    app_pg.name = 'app_tier'
> +
> +    if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +        die(f'Failed to create port groups ({txn.get_error()})')
> +
> +
> +def create_dhcp_options(idl, n):
> +    """Create DHCP options for each subnet."""
> +    for i in range(n):
> +        vlog.info(f'Creating DHCP options for node {i}')
> +        txn = ovs.db.idl.Transaction(idl)
> +        dhcp_opts = txn.insert(idl.tables['DHCP_Options'])
> +        dhcp_opts.cidr = f'10.{ip_node(i)}.0/24'
> +        dhcp_opts.setkey('options', 'server_id', f'10.{ip_node(i)}.1')
> +        dhcp_opts.setkey('options', 'server_mac', '00:00:00:00:00:01')
> +        dhcp_opts.setkey('options', 'lease_time', '3600')
> +        dhcp_opts.setkey('options', 'router', f'10.{ip_node(i)}.1')
> +        dhcp_opts.setkey('options', 'dns_server', f'10.{ip_node(i)}.2')
> +        dhcp_opts.setkey('options', 'domain_name', '"example.com"')
> +        dhcp_opts.setkey('options', 'mtu', '1500')
> +        dhcp_opts.setkey('external_ids', 'subnet', f'ls-{i}')
> +
> +        dhcpv6_opts = txn.insert(idl.tables['DHCP_Options'])
> +        dhcpv6_opts.cidr = f'fd00:{ip6_node(i)}::/64'
> +        dhcpv6_opts.setkey('options', 'server_id',
> +                           '00:00:00:00:00:01')
> +        dhcpv6_opts.setkey('options', 'dns_server',
> +                           f'fd00:{ip6_node(i)}::2')
> +        dhcpv6_opts.setkey('options', 'domain_search',
> +                           '"example.com"')
> +        dhcpv6_opts.setkey('external_ids', 'subnet',
> +                           f'ls-{i}-v6')
> +
> +        if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +            die(f'Failed to create DHCP options for node {i} '
> +                f'({txn.get_error()})')
> +
> +
> +def create_qos_rules(idl, n, switches):
> +    """Create QoS rules for bandwidth limiting and DSCP marking."""
> +    for i in range(n):
> +        vlog.info(f'Creating QoS rules for node {i}')
> +        txn = ovs.db.idl.Transaction(idl)
> +
> +        ls = switches.get(f'ls-{i}')
> +        if ls:
> +            qos_bw = txn.insert(idl.tables['QoS'])
> +            qos_bw.priority = 100
> +            qos_bw.direction = 'to-lport'
> +            qos_bw.match = f'inport == "lsp-{i}-0"'
> +            qos_bw.setkey('bandwidth', 'rate', 1000)
> +            qos_bw.setkey('bandwidth', 'burst', 100)
> +            qos_bw.setkey('external_ids', 'type', 'rate-limit')
> +            ls.addvalue('qos_rules', qos_bw.uuid)
> +
> +            qos_dscp = txn.insert(idl.tables['QoS'])
> +            qos_dscp.priority = 200
> +            qos_dscp.direction = 'from-lport'
> +            qos_dscp.match = 'ip4 && tcp.dst == 22'
> +            qos_dscp.setkey('action', 'dscp', 46)
> +            qos_dscp.setkey('external_ids', 'type', 'dscp-marking')
> +            ls.addvalue('qos_rules', qos_dscp.uuid)
> +
> +            qos_mark = txn.insert(idl.tables['QoS'])
> +            qos_mark.priority = 150
> +            qos_mark.direction = 'from-lport'
> +            qos_mark.match = 'ip4 && udp'
> +            qos_mark.setkey('action', 'mark', 1)
> +            qos_mark.setkey('external_ids', 'type', 'packet-marking')
> +            ls.addvalue('qos_rules', qos_mark.uuid)
> +
> +            qos_dscp_v6 = txn.insert(idl.tables['QoS'])
> +            qos_dscp_v6.priority = 200
> +            qos_dscp_v6.direction = 'from-lport'
> +            qos_dscp_v6.match = 'ip6 && tcp.dst == 22'
> +            qos_dscp_v6.setkey('action', 'dscp', 46)
> +            qos_dscp_v6.setkey('external_ids', 'type',
> +                               'dscp-marking-v6')
> +            ls.addvalue('qos_rules', qos_dscp_v6.uuid)
> +
> +            qos_mark_v6 = txn.insert(idl.tables['QoS'])
> +            qos_mark_v6.priority = 150
> +            qos_mark_v6.direction = 'from-lport'
> +            qos_mark_v6.match = 'ip6 && udp'
> +            qos_mark_v6.setkey('action', 'mark', 1)
> +            qos_mark_v6.setkey('external_ids', 'type',
> +                               'packet-marking-v6')
> +            ls.addvalue('qos_rules', qos_mark_v6.uuid)
> +
> +        if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +            die(f'Failed to create QoS rules for node {i}
> ({txn.get_error()})')
> +
> +
> +def create_acls_for_port_group(idl, pg_name, allowed_ports,
> priority_base):
> +    """Create ACLs for a specific port group."""
> +    txn = ovs.db.idl.Transaction(idl)
> +
> +    for row in idl.tables['Port_Group'].rows.values():
> +        if row.name == pg_name:
> +            acl_allow_est = txn.insert(idl.tables['ACL'])
> +            acl_allow_est.priority = priority_base + 100
> +            acl_allow_est.direction = 'to-lport'
> +            acl_allow_est.match = 'ct.est && !ct.rel && !ct.new &&
> !ct.inv'
> +            acl_allow_est.action = 'allow-related'
> +            row.addvalue('acls', acl_allow_est.uuid)
> +
> +            acl_allow_rel = txn.insert(idl.tables['ACL'])
> +            acl_allow_rel.priority = priority_base + 100
> +            acl_allow_rel.direction = 'to-lport'
> +            acl_allow_rel.match = 'ct.rel && !ct.est && !ct.new &&
> !ct.inv'
> +            acl_allow_rel.action = 'allow-related'
> +            row.addvalue('acls', acl_allow_rel.uuid)
> +
> +            for port in allowed_ports:
> +                acl_new = txn.insert(idl.tables['ACL'])
> +                acl_new.priority = priority_base + 50
> +                acl_new.direction = 'to-lport'
> +                acl_new.match = f'ct.new && tcp.dst == {port}'
> +                acl_new.action = 'allow-related'
> +                row.addvalue('acls', acl_new.uuid)
> +
> +            acl_drop = txn.insert(idl.tables['ACL'])
> +            acl_drop.priority = priority_base
> +            acl_drop.direction = 'to-lport'
> +            acl_drop.match = 'inport == @' + pg_name
> +            acl_drop.action = 'drop'
> +            row.addvalue('acls', acl_drop.uuid)
> +
> +            acl_arp = txn.insert(idl.tables['ACL'])
> +            acl_arp.priority = priority_base + 10
> +            acl_arp.direction = 'to-lport'
> +            acl_arp.match = 'arp || nd'
> +            acl_arp.action = 'allow'
> +            row.addvalue('acls', acl_arp.uuid)
> +            break
> +
> +    if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +        die(f'Failed to create ACLs for {pg_name} ({txn.get_error()})')
> +
> +
> +def create_acls(idl):
> +    """Create comprehensive ACLs for port groups.
> +
> +    ACL Priority Allocation:
> +      2500-2999: Security enforcement (anti-spoofing, etc.)
> +      2000-2499: Management access (SSH, ICMP, DHCP)
> +      1000-1499: Port group ACLs (security groups)
> +         1100: Connection tracking (established, related)
> +         1050: New connection per-port rules
> +         1010: ARP/ND allow
> +         1000: Default drop
> +
> +    Switch ACLs (higher priority) can override port-group ACLs, with
> +    security enforcement (anti-spoofing) taking highest priority.
> +    """
> +    vlog.info('Creating ACLs for port groups')
> +    create_acls_for_port_group(idl, 'web_tier', [80, 443], 1000)
> +    create_acls_for_port_group(idl, 'app_tier', [8080, 9000], 1000)
> +    create_acls_for_port_group(idl, 'db_tier', [5432, 3306], 1000)
> +
> +
> +def _add_acl(txn, idl, ls, priority, direction, match, action,
> +             description=None):
> +    """Create an ACL and attach it to a logical switch."""
> +    acl = txn.insert(idl.tables['ACL'])
> +    acl.priority = priority
> +    acl.direction = direction
> +    acl.match = match
> +    acl.action = action
> +    if description:
> +        acl.setkey('external_ids', 'description', description)
> +    ls.addvalue('acls', acl.uuid)
> +
> +
> +def add_acls_to_switch(idl, switch_name, node_id, switches):
> +    """Add ACLs directly to a logical switch.
> +
> +    Adds switch-level ACLs for SSH, ICMP, anti-spoofing, and DHCP.
> +    Anti-spoofing (priority 2500) prevents VMs from using IPs outside
> +    their assigned subnet, blocking IP address spoofing attacks.
> +    """
> +    txn = ovs.db.idl.Transaction(idl)
> +
> +    ls = switches.get(switch_name)
> +    if ls:
> +        _add_acl(txn, idl, ls, 2000, 'from-lport',
> +                 'tcp.dst == 22 && ip4.src == 10.0.0.0/8',
> +                 'allow', 'Allow SSH from internal')
> +        _add_acl(txn, idl, ls, 1500, 'from-lport',
> +                 'icmp4 || icmp6', 'allow')
> +        _add_acl(txn, idl, ls, 2500, 'from-lport',
> +                 f'ip4.src != 10.{ip_node(node_id)}.0/24',
> +                 'drop', 'Anti-spoofing')
> +        _add_acl(txn, idl, ls, 2000, 'from-lport',
> +                 'udp.src == 68 && udp.dst == 67', 'allow')
> +
> +        _add_acl(txn, idl, ls, 2000, 'from-lport',
> +                 'tcp.dst == 22 && ip6.src == fd00::/16',
> +                 'allow', 'Allow SSH from internal (IPv6)')
> +        _add_acl(txn, idl, ls, 2500, 'from-lport',
> +                 f'ip6.src != fd00:{ip6_node(node_id)}::/64',
> +                 'drop', 'Anti-spoofing (IPv6)')
> +        _add_acl(txn, idl, ls, 2000, 'from-lport',
> +                 'udp.src == 546 && udp.dst == 547', 'allow')
> +
> +    if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +        die(f'Failed to add ACLs to switch {switch_name} '
> +            f'({txn.get_error()})')
> +
> +
> +def create_dns_records(idl, n, switches):
> +    """Create DNS records in the NB database."""
> +    for i in range(n):
> +        vlog.info(f'Creating DNS records for node {i}')
> +        txn = ovs.db.idl.Transaction(idl)
> +        dns = txn.insert(idl.tables['DNS'])
> +        dns.setkey('records', f'web-{i}.example.com',
> +                   f'10.{ip_node(i)}.10 '
> +                   f'fd00:{ip6_node(i)}::a')
> +        dns.setkey('records', f'app-{i}.example.com',
> +                   f'10.{ip_node(i)}.30 '
> +                   f'fd00:{ip6_node(i)}::1e')
> +        dns.setkey('records', f'db-{i}.example.com',
> +                   f'10.{ip_node(i)}.20 '
> +                   f'fd00:{ip6_node(i)}::14')
> +        dns.setkey('external_ids', 'zone', f'zone-{i}')
> +
> +        ls = switches.get(f'ls-{i}')
> +        if ls:
> +            ls.addvalue('dns_records', dns.uuid)
> +
> +        if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +            die(f'Failed to create DNS records for node {i} '
> +                f'({txn.get_error()})')
> +
> +
> +def add_nat_rules(idl, n, routers):
> +    """Add NAT rules to routers.
> +
> +    Creates SNAT, DNAT, and DNAT_AND_SNAT rules to exercise all NAT code
> paths.
> +    """
> +    for i in range(n):
> +        vlog.info(f'Adding NAT rules to router {i}')
> +        txn = ovs.db.idl.Transaction(idl)
> +
> +        lr = routers.get(f'lr-{i}')
> +        if lr:
> +            nat_snat = txn.insert(idl.tables['NAT'])
> +            nat_snat.type = 'snat'
> +            nat_snat.logical_ip = f'10.{ip_node(i)}.0/24'
> +            nat_snat.external_ip = f'192.{ip_node(i)}.1'
> +            lr.addvalue('nat', nat_snat.uuid)
> +
> +            nat_dnat = txn.insert(idl.tables['NAT'])
> +            nat_dnat.type = 'dnat'
> +            nat_dnat.logical_ip = f'10.{ip_node(i)}.10'
> +            nat_dnat.external_ip = f'192.{ip_node(i)}.10'
> +            nat_dnat.setkey('external_ids', 'service', 'web')
> +            lr.addvalue('nat', nat_dnat.uuid)
> +
> +            nat_dnat_and_snat = txn.insert(idl.tables['NAT'])
> +            nat_dnat_and_snat.type = 'dnat_and_snat'
> +            nat_dnat_and_snat.logical_ip = f'10.{ip_node(i)}.20'
> +            nat_dnat_and_snat.external_ip = f'192.{ip_node(i)}.20'
> +            nat_dnat_and_snat.setkey('external_ids', 'service', 'db')
> +            lr.addvalue('nat', nat_dnat_and_snat.uuid)
> +
> +            nat_snat_v6 = txn.insert(idl.tables['NAT'])
> +            nat_snat_v6.type = 'snat'
> +            nat_snat_v6.logical_ip = f'fd00:{ip6_node(i)}::/64'
> +            nat_snat_v6.external_ip = f'fd01:{ip6_node(i)}::1'
> +            lr.addvalue('nat', nat_snat_v6.uuid)
> +
> +            nat_dnat_v6 = txn.insert(idl.tables['NAT'])
> +            nat_dnat_v6.type = 'dnat'
> +            nat_dnat_v6.logical_ip = f'fd00:{ip6_node(i)}::a'
> +            nat_dnat_v6.external_ip = f'fd01:{ip6_node(i)}::a'
> +            nat_dnat_v6.setkey('external_ids',
> +                               'service', 'web-v6')
> +            lr.addvalue('nat', nat_dnat_v6.uuid)
> +
> +            nat_ds_v6 = txn.insert(idl.tables['NAT'])
> +            nat_ds_v6.type = 'dnat_and_snat'
> +            nat_ds_v6.logical_ip = f'fd00:{ip6_node(i)}::14'
> +            nat_ds_v6.external_ip = f'fd01:{ip6_node(i)}::14'
> +            nat_ds_v6.setkey('external_ids',
> +                             'service', 'db-v6')
> +            lr.addvalue('nat', nat_ds_v6.uuid)
> +
> +        if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +            die(f'Failed to add NAT rules for node {i} '
> +                f'({txn.get_error()})')
> +
> +
> +def add_static_routes(idl, n, routers):
> +    """Add static routes to routers."""
> +    for i in range(n):
> +        vlog.info(f'Adding static routes to router {i}')
> +        txn = ovs.db.idl.Transaction(idl)
> +
> +        lr = routers.get(f'lr-{i}')
> +        if lr:
> +            route_default = txn.insert(
> +                idl.tables['Logical_Router_Static_Route'])
> +            route_default.ip_prefix = '0.0.0.0/0'
> +            route_default.nexthop = '10.0.0.1'
> +            route_default.setkey('external_ids', 'type', 'default')
> +            lr.addvalue('static_routes', route_default.uuid)
> +
> +            route_specific = txn.insert(
> +                idl.tables['Logical_Router_Static_Route'])
> +            route_specific.ip_prefix = f'172.{ip_node(i)}.0/24'
> +            route_specific.nexthop = f'10.{ip_node(i)}.254'
> +            route_specific.setkey('external_ids', 'type', 'specific')
> +            lr.addvalue('static_routes', route_specific.uuid)
> +
> +            route_discard = txn.insert(
> +                idl.tables['Logical_Router_Static_Route'])
> +            route_discard.ip_prefix = '192.0.2.0/24'
> +            route_discard.nexthop = 'discard'
> +            route_discard.setkey('external_ids', 'type', 'blackhole')
> +            lr.addvalue('static_routes', route_discard.uuid)
> +
> +            route_default_v6 = txn.insert(
> +                idl.tables['Logical_Router_Static_Route'])
> +            route_default_v6.ip_prefix = '::/0'
> +            route_default_v6.nexthop = 'fd00::1'
> +            route_default_v6.setkey('external_ids', 'type',
> +                                    'default-v6')
> +            lr.addvalue('static_routes', route_default_v6.uuid)
> +
> +            route_specific_v6 = txn.insert(
> +                idl.tables['Logical_Router_Static_Route'])
> +            route_specific_v6.ip_prefix = (
> +                f'fd02:{ip6_node(i)}::/64')
> +            route_specific_v6.nexthop = (
> +                f'fd00:{ip6_node(i)}::fe')
> +            route_specific_v6.setkey('external_ids', 'type',
> +                                     'specific-v6')
> +            lr.addvalue('static_routes',
> +                        route_specific_v6.uuid)
> +
> +            route_discard_v6 = txn.insert(
> +                idl.tables['Logical_Router_Static_Route'])
> +            route_discard_v6.ip_prefix = '2001:db8::/32'
> +            route_discard_v6.nexthop = 'discard'
> +            route_discard_v6.setkey('external_ids', 'type',
> +                                    'blackhole-v6')
> +            lr.addvalue('static_routes',
> +                        route_discard_v6.uuid)
> +
> +        if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +            die(f'Failed to add static routes for node {i} '
> +                f'({txn.get_error()})')
> +
> +
> +def add_routing_policies(idl, n, routers):
> +    """Add routing policies to routers."""
> +    for i in range(n):
> +        vlog.info(f'Adding routing policies to router {i}')
> +        txn = ovs.db.idl.Transaction(idl)
> +
> +        lr = routers.get(f'lr-{i}')
> +        if lr:
> +            policy_reroute =
> txn.insert(idl.tables['Logical_Router_Policy'])
> +            policy_reroute.priority = 100
> +            policy_reroute.match = f'ip4.src == 10.{ip_node(i)}.0/24'
> +            policy_reroute.action = 'reroute'
> +            policy_reroute.nexthops = [f'10.{ip_node((i + 1) % n)}.1']
> +            policy_reroute.setkey('external_ids', 'policy',
> +                                  'traffic-engineering')
> +            lr.addvalue('policies', policy_reroute.uuid)
> +
> +            policy_allow = txn.insert(idl.tables['Logical_Router_Policy'])
> +            policy_allow.priority = 50
> +            policy_allow.match = 'ip4.dst == $trusted_networks'
> +            policy_allow.action = 'allow'
> +            lr.addvalue('policies', policy_allow.uuid)
> +
> +            policy_reroute_v6 = txn.insert(
> +                idl.tables['Logical_Router_Policy'])
> +            policy_reroute_v6.priority = 100
> +            policy_reroute_v6.match = (
> +                f'ip6.src == fd00:{ip6_node(i)}::/64')
> +            policy_reroute_v6.action = 'reroute'
> +            policy_reroute_v6.nexthops = [
> +                f'fd00:{ip6_node((i + 1) % n)}::1']
> +            policy_reroute_v6.setkey(
> +                'external_ids', 'policy',
> +                'traffic-engineering-v6')
> +            lr.addvalue('policies',
> +                        policy_reroute_v6.uuid)
> +
> +            policy_allow_v6 = txn.insert(
> +                idl.tables['Logical_Router_Policy'])
> +            policy_allow_v6.priority = 50
> +            policy_allow_v6.match = (
> +                'ip6.dst == $trusted_networks')
> +            policy_allow_v6.action = 'allow'
> +            lr.addvalue('policies', policy_allow_v6.uuid)
> +
> +        if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +            die(f'Failed to add routing policies for node {i} '
> +                f'({txn.get_error()})')
> +
> +
> +def _add_ports(txn, idl, s, i, ports_per_switch):
> +    for p in range(ports_per_switch):
> +        lsp = txn.insert(idl.tables['Logical_Switch_Port'])
> +        lsp.name = f'lsp-{i}-{p}'
> +        mac_byte = (p + 10) % 256
> +        mac = (f'00:00:{i >> 8:02x}:{i & 0xff:02x}'
> +               f':{p:02x}:{mac_byte:02x}')
> +        ip = f'10.{ip_node(i)}.{10 + p}'
> +        ip6 = f'fd00:{ip6_node(i)}::{10 + p:x}'
> +        lsp.addresses = [f'{mac} {ip} {ip6}']
> +        lsp.port_security = [f'{mac} {ip} {ip6}']
> +        lsp.setkey('external_ids', 'vm-id', f'vm-{i}-{p}')
> +
> +        if p % 3 == 0:
> +            lsp.setkey('external_ids', 'tier', 'web')
> +        elif p % 3 == 1:
> +            lsp.setkey('external_ids', 'tier', 'app')
> +        else:
> +            lsp.setkey('external_ids', 'tier', 'db')
> +
> +        s.addvalue('ports', lsp.uuid)
> +
> +
> +def _add_node(txn, idl, i, chassis, cluster_rtr, join_sw, lbg,
> +              ports_per_switch):
> +    gwr = txn.insert(idl.tables['Logical_Router'])
> +    gwr.name = f'lr-{i}'
> +    gwr.addvalue('load_balancer_group', lbg.uuid)
> +    gwr.setkey('options', 'chassis', chassis)
> +
> +    gwr2join = txn.insert(idl.tables['Logical_Router_Port'])
> +    gwr2join.name = f'lr2j-{i}'
> +    gwr2join.mac = '00:00:00:00:00:01'
> +    gwr2join.networks = ['10.0.0.1/8', 'fd00::1/48']
> +    gwr.addvalue('ports', gwr2join.uuid)
> +
> +    join2gwr = txn.insert(idl.tables['Logical_Switch_Port'])
> +    join2gwr.name = f'j2lr-{i}'
> +    join2gwr.type = 'router'
> +    join2gwr.addresses = ['router']
> +    join2gwr.setkey('options', 'router-port', gwr2join.name)
> +    join_sw.addvalue('ports', join2gwr.uuid)
> +
> +    s = txn.insert(idl.tables['Logical_Switch'])
> +    s.name = f'ls-{i}'
> +    s.addvalue('load_balancer_group', lbg.uuid)
> +    s.setkey('other_config', 'subnet', f'10.{ip_node(i)}.0/24')
> +    s.setkey('other_config', 'mcast_snoop', 'true')
> +
> +    cluster2s = txn.insert(idl.tables['Logical_Router_Port'])
> +    cluster2s.name = f'c2s-{i}'
> +    cluster2s.mac = '00:00:00:00:00:01'
> +    cluster2s.networks = [f'10.{ip_node(i)}.1/24',
> +                          f'fd00:{ip6_node(i)}::1/64']
> +    cluster_rtr.addvalue('ports', cluster2s.uuid)
> +
> +    gw_chassis = txn.insert(idl.tables['Gateway_Chassis'])
> +    gw_chassis.name = f'{cluster2s.name}-{chassis}'
> +    gw_chassis.chassis_name = chassis
> +    gw_chassis.priority = 1
> +    cluster2s.addvalue('gateway_chassis', gw_chassis.uuid)
> +
> +    s2cluster = txn.insert(idl.tables['Logical_Switch_Port'])
> +    s2cluster.name = f's2c-{i}'
> +    s2cluster.type = 'router'
> +    s2cluster.addresses = ['router']
> +    s2cluster.setkey('options', 'router-port', cluster2s.name)
> +    s.addvalue('ports', s2cluster.uuid)
> +
> +    _add_ports(txn, idl, s, i, ports_per_switch)
> +
> +
> +def create_topology(idl, n, ports_per_switch, batch_size):
> +    """Create the basic topology with routers, switches, and ports."""
> +    vlog.info('Creating topology')
> +    txn = ovs.db.idl.Transaction(idl)
> +    lbg = txn.insert(idl.tables['Load_Balancer_Group'])
> +    lbg.name = 'lbg'
> +
> +    vlog.info('Adding join switch')
> +    join_sw = txn.insert(idl.tables['Logical_Switch'])
> +    join_sw.name = 'join'
> +
> +    cluster_rtr = txn.insert(idl.tables['Logical_Router'])
> +    cluster_rtr.name = 'cluster'
> +
> +    rcj = txn.insert(idl.tables['Logical_Router_Port'])
> +    rcj.name = 'rcj'
> +    rcj.mac = '00:00:00:00:00:01'
> +    rcj.networks = ['10.0.0.1/8', 'fd00::1/48']
> +    cluster_rtr.addvalue('ports', rcj.uuid)
> +
> +    sjc = txn.insert(idl.tables['Logical_Switch_Port'])
> +    sjc.name = 'sjc'
> +    sjc.type = 'router'
> +    sjc.addresses = ['router']
> +    sjc.setkey('options', 'router-port', 'rcj')
> +    join_sw.addvalue('ports', sjc.uuid)
> +
> +    for i in range(n):
> +        vlog.info(f'Provisioning node {i}')
> +        chassis = f'chassis-{i // batch_size}'
> +        _add_node(txn, idl, i, chassis, cluster_rtr, join_sw, lbg,
> +                  ports_per_switch)
> +
> +    if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +        die(f'Failed to create topology ({txn.get_error()})')
> +
> +
> +def assign_ports_to_groups(idl):
> +    """Assign logical switch ports to port groups based on tier."""
> +    vlog.info('Assigning ports to port groups')
> +
> +    tier_ports = {'web': [], 'app': [], 'db': []}
> +
> +    for row in idl.tables['Logical_Switch_Port'].rows.values():
> +        tier = row.external_ids.get('tier')
> +        if tier in tier_ports:
> +            tier_ports[tier].append(row.uuid)
> +
> +    txn = ovs.db.idl.Transaction(idl)
> +    tier_to_group = {'web': 'web_tier', 'app': 'app_tier', 'db':
> 'db_tier'}
> +    for row in idl.tables['Port_Group'].rows.values():
> +        for tier, group_name in tier_to_group.items():
> +            if row.name == group_name:
> +                for port_uuid in tier_ports[tier]:
> +                    row.addvalue('ports', port_uuid)
> +
> +    if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +        die(f'Failed to assign ports to groups ({txn.get_error()})')
> +
> +
> +def add_explicit_lbs(idl, n, n_vips, n_backends, routers, switches):
> +    """Add explicit (non-templated) load balancers.
> +
> +    Uses explicit (non-templated) LBs to maximize memory usage for
> +    regression testing.
> +    """
> +    for i in range(n):
> +        lr = routers.get(f'lr-{i}')
> +        ls = switches.get(f'ls-{i}')
> +        for j in range(n_vips):
> +            vlog.info(f'Adding LB {j} for node {i}')
> +            txn = ovs.db.idl.Transaction(idl)
> +            port = j + 1
> +            j1 = (j + 1) // 250
> +            j2 = (j + 1) % 250
> +
> +            lb = txn.insert(idl.tables['Load_Balancer'])
> +            lb.name = f'lb-{j}-{i}'
> +            lb.setkey('vips', f'42.42.{ip_node(i)}:{port}',
> +                      ','.join(f'42.{k}.{j1}.{j2}:{port}'
> +                               for k in range(n_backends)))
> +            lb.setkey(
> +                'vips',
> +                f'[fd42::{ip6_node(i)}:{j:x}]:{port}',
> +                ','.join(f'[fd42:{k:x}::{j1:x}:{j2:x}]:{port}'
> +                         for k in range(n_backends)))
> +            lb.protocol = 'tcp'
> +            lr.addvalue('load_balancer', lb.uuid)
> +            ls.addvalue('load_balancer', lb.uuid)
> +            if txn.commit_block() != ovs.db.idl.Transaction.SUCCESS:
> +                die(f'Failed to add LB ({txn.get_error()})')
> +
> +
> +def run(remote, n, n_vips, n_backends, ports_per_switch, batch_size):
> +    """Main execution function."""
> +    schema_helper = ovs.db.idl.SchemaHelper(SCHEMA)
> +    schema_helper.register_all()
> +    idl = ovs.db.idl.Idl(remote, schema_helper, leader_only=False)
> +
> +    err, stream = ovs.stream.Stream.open_block(
> +        ovs.stream.Stream.open(remote), 2000
> +    )
> +    if err:
> +        sys.stderr.write(f'failed to connect to "{remote}"')
> +        sys.exit(1)
> +
> +    if not stream:
> +        sys.stderr.write(f'failed to connect to "{remote}"')
> +        sys.exit(1)
> +    rpc = ovs.jsonrpc.Connection(stream)
> +
> +    while idl.change_seqno == 0 and not idl.run():
> +        rpc.run()
> +
> +        poller = ovs.poller.Poller()
> +        idl.wait(poller)
> +        rpc.wait(poller)
> +        poller.block()
> +
> +    # Check if database is clean before proceeding
> +    if (len(idl.tables['Load_Balancer_Group'].rows) > 0 or
> +        len(idl.tables['Logical_Switch'].rows) > 0 or
> +        len(idl.tables['Logical_Router'].rows) > 0):
> +        die('Database is not empty. Please restart the sandbox or clear
> the '
> +            'database before running this script.')
> +
> +    create_topology(idl, n, ports_per_switch, batch_size)
> +
> +    # Build lookup dictionaries for O(1) access to switches and routers
> +    switches = {row.name: row
> +                for row in idl.tables['Logical_Switch'].rows.values()}
> +    routers = {row.name: row
> +               for row in idl.tables['Logical_Router'].rows.values()}
> +
> +    create_address_sets(idl, n)
> +    create_port_groups(idl)
> +    assign_ports_to_groups(idl)
> +    create_dhcp_options(idl, n)
> +    create_dns_records(idl, n, switches)
> +    add_nat_rules(idl, n, routers)
> +    add_static_routes(idl, n, routers)
> +    create_acls(idl)
> +    for i in range(n):
> +        add_acls_to_switch(idl, f'ls-{i}', i, switches)
> +    create_qos_rules(idl, n, switches)
> +    add_routing_policies(idl, n, routers)
> +    add_explicit_lbs(idl, n, n_vips, n_backends, routers, switches)
> +
> +
> +def main():
> +    """Parse arguments and run the benchmark."""
> +    parser = argparse.ArgumentParser(
> +        description='Create a complex OVN topology with various features'
> +    )
> +    parser.add_argument(
> +        '-r', '--remote', required=True, help='NB connection string'
> +    )
> +    parser.add_argument(
> +        '-n', '--nodes', type=int, required=True, help='Number of nodes'
> +    )
> +    parser.add_argument(
> +        '-p',
> +        '--ports-per-switch',
> +        type=int,
> +        default=9,
> +        help='Number of logical switch ports per switch (default: 9, '
> +             'provides 3 ports per tier)',
> +    )
> +    parser.add_argument(
> +        '-v', '--vips', type=int, default=5,
> +        help='Number of LB VIPs per node (default: 5)'
> +    )
> +    parser.add_argument(
> +        '-B',
> +        '--backends',
> +        type=int,
> +        default=5,
> +        help='Number backends per VIP (default: 5)',
> +    )
> +    parser.add_argument(
> +        '-b',
> +        '--batch-size',
> +        type=int,
> +        default=0,
> +        help='Nodes per chassis (default: n/10)',
> +    )
> +    parser.add_argument(
> +        '-d', '--debug',
> +        action='store_true',
> +        help='Enable debug output (show info messages)',
> +    )
> +    args = parser.parse_args()
> +
> +    if args.batch_size <= 0:
> +        args.batch_size = max(1, args.nodes // 10)
> +
> +    if args.nodes > 65535:
> +        sys.stderr.write('Error: maximum supported node count is 65535\n')
> +        sys.exit(1)
> +
> +    if args.ports_per_switch > 245:
> +        sys.stderr.write(
> +            'Error: maximum supported ports per switch is 245\n')
> +        sys.exit(1)
> +
> +    if args.debug:
> +        vlog.set_levels_from_string('console:info')
> +
> +    # Print configuration summary
> +    sys.stderr.write('\n=== OVN Benchmark Configuration ===\n')
> +    sys.stderr.write(f'Nodes:                        {args.nodes}'
> +                     f' ({args.nodes} routers + {args.nodes} switches)\n')
> +    sys.stderr.write(f'Ports per switch:
>  {args.ports_per_switch} '
> +                     f'({args.ports_per_switch * args.nodes} total
> ports)\n')
> +    sys.stderr.write(f'Load balancer VIPs per node:  {args.vips} '
> +                     f'({args.vips * args.nodes} total VIPs)\n')
> +    sys.stderr.write(f'Backends per VIP:             {args.backends}\n')
> +    sys.stderr.write(f'Total load balancers:         '
> +                     f'{args.nodes * args.vips}\n')
> +    n_chassis = ((args.nodes + args.batch_size - 1)
> +                 // args.batch_size)
> +    sys.stderr.write(f'Nodes per chassis (batch):    '
> +                     f'{args.batch_size} '
> +                     f'({n_chassis} chassis)\n')
> +    sys.stderr.write(f'Debug logging:                '
> +                     f'{"enabled" if args.debug else "disabled"}\n')
> +    sys.stderr.write('===================================\n\n')
> +
> +    run(args.remote, args.nodes, args.vips, args.backends,
> +        args.ports_per_switch, args.batch_size)
> +
> +
> +if __name__ == '__main__':
> +    try:
> +        main()
> +    except error.Error as e:
> +        sys.stderr.write(f'{e}\n')
> +        sys.exit(1)
> diff --git a/tutorial/ovn-benchmark.sh b/tutorial/ovn-benchmark.sh
> new file mode 100755
> index 000000000..73df6970e
> --- /dev/null
> +++ b/tutorial/ovn-benchmark.sh
> @@ -0,0 +1,234 @@
> +#!/bin/bash
> +
> +DEFAULT_NODES=200
> +
> +PROCESS_NAME=()
> +FILE_NAME=""
> +NODES=""
> +PROCESS_PIDS=()
> +CURRENT_MEM=()
> +PEAK_MEM=()
> +FINAL_PEAK_KB=()
> +FINAL_PEAK_MB=()
> +DEBUG=false
> +BATCH_SIZE=""
> +WATCHER_PID=""
> +BENCHMARK_TMPDIR=""
> +
> +# Cleanup function to kill background watcher and remove temp files.
> +cleanup() {
> +    if [ -n "$WATCHER_PID" ]; then
> +        kill $WATCHER_PID 2>/dev/null
> +    fi
> +    if [ -n "$BENCHMARK_TMPDIR" ]; then
> +        rm -rf "$BENCHMARK_TMPDIR"
> +    fi
> +}
> +
> +# Register cleanup to run on script exit.
> +trap cleanup EXIT
> +
> +while [[ $# -gt 0 ]]; do
> +    case "$1" in
> +        -h|--help|--usage)
> +            echo "Usage: $0 [OPTIONS] [NODES] [PROCESS...]"
> +            echo ""
> +            echo "Arguments:"
> +            echo "  NODES         Number of nodes to create" \
> +                 "(default: $DEFAULT_NODES)"
> +            echo "  PROCESS       Process(es) to track:" \
> +                 "ovn-northd, ovn-controller"
> +            echo "                (default: both)"
> +            echo ""
> +            echo "Options:"
> +            echo "  -f, --file FILE       Load NB database from" \
> +                 "file instead of generating"
> +            echo "  -b, --batch-size N    Nodes per chassis" \
> +                 "(default: NODES/10)"
> +            echo "  -d, --debug           Enable debug output"
> +            echo "  -h, --help            Show this help message"
> +            echo ""
> +            echo "Examples:"
> +            echo "  $0                      # 200 nodes, track both
> processes"
> +            echo "  $0 50                   # 50 nodes"
> +            echo "  $0 50 ovn-northd        # 50 nodes, track only
> ovn-northd"
> +            echo "  $0 --debug 20           # 20 nodes with debug output"
> +            echo "  $0 --file ovnnb_db.db   # Load from file"
> +            echo "  $0 50 -b 10             # 50 nodes, 10 per chassis"
> +            exit 0
> +            ;;
> +        -b|--batch-size)
> +            BATCH_SIZE="$2"
> +            shift 2
> +            ;;
> +        -d|--debug)
> +            DEBUG=true
> +            shift
> +            ;;
> +        -f|--file)
> +            FILE_NAME="$2"
> +            shift 2
> +            ;;
> +        -*)
> +            echo "Unknown option: $1"
> +            exit 1
> +            ;;
> +        *)
> +            if [ -z "$NODES" ]; then
> +                NODES="$1"
> +            else
> +                # Normalize process names: accept both "northd" and
> +                # "ovn-northd".
> +                case "$1" in
> +                    northd)
> +                        PROCESS_NAME+=("ovn-northd")
> +                        ;;
> +                    controller)
> +                        PROCESS_NAME+=("ovn-controller")
> +                        ;;
> +                    *)
> +                        PROCESS_NAME+=("$1")
> +                        ;;
> +                esac
> +            fi
> +            shift
> +            ;;
> +    esac
> +done
> +
> +# Apply defaults if not set by user.
> +NODES=${NODES:-$DEFAULT_NODES}
> +
> +if [ -z "$BATCH_SIZE" ]; then
> +    BATCH_SIZE=$((NODES / 10))
> +fi
> +if [ "$BATCH_SIZE" -lt 1 ]; then
> +    BATCH_SIZE=1
> +fi
> +
> +# Track both processes if not specified.
> +if [ ${#PROCESS_NAME[@]} -eq 0 ]; then
> +    PROCESS_NAME=("ovn-controller" "ovn-northd")
> +fi
> +
> +if [ "$DEBUG" = true ]; then
> +    echo "Nodes:       $NODES"
> +    echo "Batch size:  $BATCH_SIZE"
> +    echo "Processes:   ${PROCESS_NAME[*]}"
> +    echo "File:        ${FILE_NAME:-None}"
> +fi
> +
> +for pn in "${PROCESS_NAME[@]}"; do
> +    pid=$(pgrep -x "$pn" | head -n 1)
> +    if [ -z "$pid" ]; then
> +        echo "Error: Could not find process matching '$pn'"
> +        exit 1
> +    fi
> +    PROCESS_PIDS+=("$pid")
> +done
> +
> +if [ "$DEBUG" = true ]; then
> +    for i in "${!PROCESS_NAME[@]}"; do
> +        echo "Tracking memory for ${PROCESS_NAME[$i]}" \
> +             "(PID: ${PROCESS_PIDS[$i]})"
> +    done
> +fi
> +
> +# Create a temporary file to store the highest memory value we see.
> +BENCHMARK_TMPDIR=$(mktemp -d)
> +for pn in "${PROCESS_NAME[@]}"; do
> +    echo 0 > "$BENCHMARK_TMPDIR/peak_mem_$pn.txt"
> +done
> +
> +
> +# Start the background "Watcher" loop.
> +while true; do
> +    for i in "${!PROCESS_NAME[@]}"; do
> +        pn="${PROCESS_NAME[$i]}"
> +        pid="${PROCESS_PIDS[$i]}"
> +
> +        # Get the Resident Set Size (RSS) memory in KB.
> +        CURRENT_MEM[$i]=$(ps -p $pid -o rss= 2>/dev/null)
> +
> +        # If the process died, break out of both loops.
> +        if [ -z "${CURRENT_MEM[$i]}" ]; then break 2; fi
> +
> +        PEAK_MEM[$i]=$(cat "$BENCHMARK_TMPDIR/peak_mem_$pn.txt")
> +
> +        if [ "${CURRENT_MEM[$i]}" -gt "${PEAK_MEM[$i]}" ]; then
> +            echo "${CURRENT_MEM[$i]}" >
> "$BENCHMARK_TMPDIR/peak_mem_$pn.txt"
> +        fi
> +    done
> +
> +    sleep 0.5
> +done &
> +
> +WATCHER_PID=$!
> +
> +START_TIME=$(date +%s%2N)
> +
> +if [ "$DEBUG" = true ]; then
> +    DEBUG_FLAG="-d"
> +else
> +    DEBUG_FLAG=""
> +fi
> +
> +# Load database from file or generate with Python script.
> +if [ -n "$FILE_NAME" ]; then
> +    echo "Loading database from file: $FILE_NAME"
> +    if [ ! -f "$FILE_NAME" ]; then
> +        echo "Error: File '$FILE_NAME' not found"
> +        exit 1
> +    fi
> +    ovsdb-client restore unix:$PWD/sandbox/nb1.ovsdb < "$FILE_NAME"
> +else
> +    echo "Generating database with Python script"
> +    python3 ovn-benchmark.py -n $NODES -b $BATCH_SIZE \
> +        -r unix:$PWD/sandbox/nb1.ovsdb $DEBUG_FLAG
> +    if [ $? -ne 0 ]; then
> +        echo "Error: Failed to generate database"
> +        exit 1
> +    fi
> +fi
> +
> +# Bind the first port of each switch assigned to chassis-0.
> +for i in $(seq 0 $((BATCH_SIZE - 1))); do
> +    ovs-vsctl add-port br-int lsp-${i}-0 -- \
> +        set interface lsp-${i}-0 \
> +        external_ids:iface-id=lsp-${i}-0
> +done
> +
> +# Wait for ovn-controller to claim ports and finish processing.
> +ovn-nbctl --wait=hv sync
> +
> +ovs-appctl -t $PWD/sandbox/nb1 ovsdb-server/compact
> +ovs-appctl -t $PWD/sandbox/sb1 ovsdb-server/compact
> +
> +END_TIME=$(date +%s%2N)
> +
> +kill $WATCHER_PID 2>/dev/null
> +wait $WATCHER_PID 2>/dev/null
> +
> +ELAPSED_TIME=$((END_TIME - START_TIME))
> +ELAPSED_SECS=$((ELAPSED_TIME / 100))
> +ELAPSED_HSECS=$((ELAPSED_TIME % 100))
> +
> +for i in "${!PROCESS_NAME[@]}"; do
> +    pn=${PROCESS_NAME[$i]}
> +    FINAL_PEAK_KB[$i]=$(cat "$BENCHMARK_TMPDIR/peak_mem_$pn.txt")
> +    FINAL_PEAK_MB[$i]=$((FINAL_PEAK_KB[$i] / 1024))
> +done
> +
> +echo ""
> +echo "=== Benchmark Results ==="
> +printf "Total time:                  %d.%02d seconds\n" \
> +    $ELAPSED_SECS $ELAPSED_HSECS
> +
> +for i in "${!PROCESS_NAME[@]}"; do
> +    printf "%-28s %s MB\n" \
> +        "${PROCESS_NAME[$i]} peak memory:" "${FINAL_PEAK_MB[$i]}"
> +done
> +echo "========================="
> +echo ""
> +
> +# Cleanup handled by trap on EXIT.
> --
> 2.54.0
>
> _______________________________________________
> dev mailing list
> [email protected]
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
>
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to