From b093e59ec1442b0a5542d807c6e9160df1e82584 Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Thu, 18 Dec 2025 11:28:57 +1100 Subject: [PATCH 01/11] feature: add launch.json for tab completion --- .vscode/launch.json | 19 +++++++++++++++++++ .../azure/cli/core/commands/__init__.py | 1 + 2 files changed, 20 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index e7ed7a11353..cd23a7aa89b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,6 +38,25 @@ "--help" ], "console": "integratedTerminal", + }, + { + "name": "Azure CLI Debug Tab Completion (External Console)", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/azure-cli/azure/cli/__main__.py", + "args": [], + "console": "externalTerminal", + "cwd": "${workspaceFolder}", + "env": { + "_ARGCOMPLETE": "1", + "COMP_LINE": "az vm ", + "COMP_POINT": "6", + "_ARGCOMPLETE_SUPPRESS_SPACE": "0", + "_ARGCOMPLETE_IFS": "\n", + "_ARGCOMPLETE_SHELL": "powershell", + "ARGCOMPLETE_USE_TEMPFILES": "1" + }, + "justMyCode": false } ] } \ No newline at end of file diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index fb2a9a3dece..23b97b89203 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -517,6 +517,7 @@ def execute(self, args): args = _pre_command_table_create(self.cli_ctx, args) self.cli_ctx.raise_event(EVENT_INVOKER_PRE_CMD_TBL_CREATE, args=args) + # @TODO: this is one bottleneck self.commands_loader.load_command_table(args) self.cli_ctx.raise_event(EVENT_INVOKER_PRE_CMD_TBL_TRUNCATE, load_cmd_tbl_func=self.commands_loader.load_command_table, args=args) From 7c730750db5e60b348db2678cb985210f8aab558 Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Fri, 19 Dec 2025 14:36:50 +1100 Subject: [PATCH 02/11] wip: debugging statements --- .vscode/launch.json | 3 ++- src/azure-cli-core/azure/cli/core/__init__.py | 10 ++++++++++ .../azure/cli/core/commands/__init__.py | 10 ++++++++++ src/azure-cli/azure/cli/__main__.py | 12 ++++++++++++ .../azure/cli/command_modules/vm/__init__.py | 3 ++- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index cd23a7aa89b..bfb73c66429 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -54,7 +54,8 @@ "_ARGCOMPLETE_SUPPRESS_SPACE": "0", "_ARGCOMPLETE_IFS": "\n", "_ARGCOMPLETE_SHELL": "powershell", - "ARGCOMPLETE_USE_TEMPFILES": "1" + "ARGCOMPLETE_USE_TEMPFILES": "1", + "_ARGCOMPLETE_STDOUT_FILENAME": "C:\\temp\\az_debug_completion.txt" }, "justMyCode": false } diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index ae0f253caee..4ca6d7e25b5 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -434,9 +434,19 @@ def _get_extension_suppressions(mod_loaders): index_modules, index_extensions = index_result # Always load modules and extensions, because some of them (like those in # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core + import time + import sys + start_time = time.time() + print(f"[PERF] Loading modules from index: {index_modules}", file=sys.stderr, flush=True) _update_command_table_from_modules(args, index_modules) + elapsed_modules = time.time() - start_time + print(f"[PERF] Loaded modules in {elapsed_modules:.3f} seconds", file=sys.stderr, flush=True) + # The index won't contain suppressed extensions + start_time = time.time() _update_command_table_from_extensions([], index_extensions) + elapsed_extensions = time.time() - start_time + print(f"[PERF] Loaded extensions in {elapsed_extensions:.3f} seconds", file=sys.stderr, flush=True) logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table)) from azure.cli.core.util import roughly_parse_command diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 23b97b89203..57dabece368 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -506,6 +506,13 @@ class AzCliCommandInvoker(CommandInvoker): # pylint: disable=too-many-statements,too-many-locals,too-many-branches def execute(self, args): + import time + import sys + import os + _start_time = time.time() + if os.environ.get('_ARGCOMPLETE'): + print(f"[PERF] Starting completion at {_start_time}", file=sys.stderr, flush=True) + from knack.events import (EVENT_INVOKER_PRE_CMD_TBL_CREATE, EVENT_INVOKER_POST_CMD_TBL_CREATE, EVENT_INVOKER_CMD_TBL_LOADED, EVENT_INVOKER_PRE_PARSE_ARGS, EVENT_INVOKER_POST_PARSE_ARGS, @@ -586,6 +593,9 @@ def execute(self, args): if args[0].lower() == 'help': args[0] = '--help' + if os.environ.get('_ARGCOMPLETE'): + elapsed = time.time() - _start_time + print(f"[PERF] About to enable autocomplete. Elapsed so far: {elapsed:.3f} seconds", file=sys.stderr, flush=True) self.parser.enable_autocomplete() self.cli_ctx.raise_event(EVENT_INVOKER_PRE_PARSE_ARGS, args=args) diff --git a/src/azure-cli/azure/cli/__main__.py b/src/azure-cli/azure/cli/__main__.py index dd35d5a0345..1a8b61fc84e 100644 --- a/src/azure-cli/azure/cli/__main__.py +++ b/src/azure-cli/azure/cli/__main__.py @@ -9,6 +9,11 @@ start_time = timeit.default_timer() import sys +import os + +# Track completion performance +if os.environ.get('_ARGCOMPLETE'): + print(f"[PERF] az CLI entry point at {start_time}", file=sys.stderr, flush=True) from azure.cli.core import telemetry from azure.cli.core import get_default_cli @@ -29,6 +34,10 @@ def cli_main(cli, args): az_cli = get_default_cli() +if os.environ.get('_ARGCOMPLETE'): + elapsed = timeit.default_timer() - start_time + print(f"[PERF] CLI initialized in {elapsed:.3f} seconds", file=sys.stderr, flush=True) + telemetry.set_application(az_cli, ARGCOMPLETE_ENV_NAME) # Log the init finish time @@ -38,6 +47,9 @@ def cli_main(cli, args): try: telemetry.start() + if os.environ.get('_ARGCOMPLETE'): + print(f"[PERF] Calling cli_main() at {timeit.default_timer():.3f}", file=sys.stderr, flush=True) + exit_code = cli_main(az_cli, sys.argv[1:]) if exit_code == 0: diff --git a/src/azure-cli/azure/cli/command_modules/vm/__init__.py b/src/azure-cli/azure/cli/command_modules/vm/__init__.py index 42d6e85ca60..6d5f1ec1918 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/vm/__init__.py @@ -13,7 +13,8 @@ from azure.cli.core import AzCommandsLoader from azure.cli.core.profiles import ResourceType -import azure.cli.command_modules.vm._help # pylint: disable=unused-import +# Don't import help during tab completion - it's expensive and not needed +# import azure.cli.command_modules.vm._help # pylint: disable=unused-import class ComputeCommandsLoader(AzCommandsLoader): From 9ca594221bbbf283b2313b7b6b04c15ae820fe78 Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Tue, 23 Dec 2025 12:42:50 +1100 Subject: [PATCH 03/11] wip: add debug statements --- src/azure-cli-core/azure/cli/core/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 4ca6d7e25b5..09228029603 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -427,15 +427,16 @@ def _get_extension_suppressions(mod_loaders): command_index = None # Set fallback=False to turn off command index in case of regression use_command_index = self.cli_ctx.config.getboolean('core', 'use_command_index', fallback=True) + print(f"[PERF] use_command_index: {use_command_index}, args: {args}", file=sys.stderr, flush=True) if use_command_index: command_index = CommandIndex(self.cli_ctx) index_result = command_index.get(args) + print(f"[PERF] index_result: {index_result}", file=sys.stderr, flush=True) if index_result: index_modules, index_extensions = index_result # Always load modules and extensions, because some of them (like those in # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core import time - import sys start_time = time.time() print(f"[PERF] Loading modules from index: {index_modules}", file=sys.stderr, flush=True) _update_command_table_from_modules(args, index_modules) From 50a9fa9e1b31336865d1c19ad08f16c96acf893c Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Tue, 23 Dec 2025 12:54:45 +1100 Subject: [PATCH 04/11] wip: --- src/azure-cli/azure/cli/command_modules/vm/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/vm/__init__.py b/src/azure-cli/azure/cli/command_modules/vm/__init__.py index 6d5f1ec1918..42d6e85ca60 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/vm/__init__.py @@ -13,8 +13,7 @@ from azure.cli.core import AzCommandsLoader from azure.cli.core.profiles import ResourceType -# Don't import help during tab completion - it's expensive and not needed -# import azure.cli.command_modules.vm._help # pylint: disable=unused-import +import azure.cli.command_modules.vm._help # pylint: disable=unused-import class ComputeCommandsLoader(AzCommandsLoader): From b3f450e0ba21ba2cbaa3c3390873241a41c03dd4 Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Tue, 23 Dec 2025 15:06:15 +1100 Subject: [PATCH 05/11] wip: more benchmarking comments --- .vscode/launch.json | 4 ++-- src/azure-cli-core/azure/cli/core/__init__.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index bfb73c66429..82544dba4de 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -49,8 +49,8 @@ "cwd": "${workspaceFolder}", "env": { "_ARGCOMPLETE": "1", - "COMP_LINE": "az vm ", - "COMP_POINT": "6", + "COMP_LINE": "az ", + "COMP_POINT": "3", "_ARGCOMPLETE_SUPPRESS_SPACE": "0", "_ARGCOMPLETE_IFS": "\n", "_ARGCOMPLETE_SHELL": "powershell", diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 09228029603..c8c55eab848 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -422,12 +422,16 @@ def _get_extension_suppressions(mod_loaders): self.command_table.clear() # Import announced breaking changes in azure.cli.core._breaking_change.py + import time + t1 = time.time() import_core_breaking_changes() + print(f"[PERF] Breaking changes import: {time.time()-t1:.3f}s", file=sys.stderr, flush=True) command_index = None # Set fallback=False to turn off command index in case of regression + t1 = time.time() use_command_index = self.cli_ctx.config.getboolean('core', 'use_command_index', fallback=True) - print(f"[PERF] use_command_index: {use_command_index}, args: {args}", file=sys.stderr, flush=True) + print(f"[PERF] use_command_index: {use_command_index}, args: {args}, lookup time: {time.time()-t1:.3f}s", file=sys.stderr, flush=True) if use_command_index: command_index = CommandIndex(self.cli_ctx) index_result = command_index.get(args) @@ -495,16 +499,25 @@ def _get_extension_suppressions(mod_loaders): # No module found from the index. Load all command modules and extensions logger.debug("Loading all modules and extensions") + t1 = time.time() + print(f"[PERF] Loading ALL modules (no index)", file=sys.stderr, flush=True) _update_command_table_from_modules(args) + elapsed_all_modules = time.time() - t1 + print(f"[PERF] Loaded ALL modules in {elapsed_all_modules:.3f} seconds", file=sys.stderr, flush=True) + t1 = time.time() ext_suppressions = _get_extension_suppressions(self.loaders) # We always load extensions even if the appropriate module has been loaded # as an extension could override the commands already loaded. _update_command_table_from_extensions(ext_suppressions) + elapsed_all_extensions = time.time() - t1 + print(f"[PERF] Loaded ALL extensions in {elapsed_all_extensions:.3f} seconds", file=sys.stderr, flush=True) logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table)) if use_command_index: + t1 = time.time() command_index.update(self.command_table) + print(f"[PERF] Command index update: {time.time()-t1:.3f}s", file=sys.stderr, flush=True) return self.command_table From d4809c280bee15cfcf25779e59de04fbab52da9d Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Tue, 23 Dec 2025 15:42:10 +1100 Subject: [PATCH 06/11] feature: adding top-level command lookup from commandIndex.json --- src/azure-cli-core/azure/cli/core/__init__.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index c8c55eab848..c959018c41b 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -438,6 +438,25 @@ def _get_extension_suppressions(mod_loaders): print(f"[PERF] index_result: {index_result}", file=sys.stderr, flush=True) if index_result: index_modules, index_extensions = index_result + + # Special case for top-level completion - create minimal command groups + if index_modules == '__top_level_completion__': + import time + from azure.cli.core.commands import AzCliCommand + start_time = time.time() + print(f"[PERF] Top-level completion mode: creating {len(index_extensions)} command stubs", file=sys.stderr, flush=True) + # index_extensions contains the command names, not extensions + for cmd_name in index_extensions: + # Create a minimal command entry for tab completion + # This allows argparse to see the command without loading the module + if cmd_name not in self.command_table: + self.command_table[cmd_name] = AzCliCommand( + self, cmd_name, lambda: None + ) + elapsed = time.time() - start_time + print(f"[PERF] Created command stubs in {elapsed:.3f} seconds", file=sys.stderr, flush=True) + return self.command_table + # Always load modules and extensions, because some of them (like those in # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core import time @@ -619,8 +638,14 @@ def get(self, args): return None # Make sure the top-level command is provided, like `az version`. - # Skip command index for `az` or `az --help`. + # For top-level completion (az [tab]), use a special marker to skip module loading if not args or args[0].startswith('-'): + if not args and self.cli_ctx.data.get('completer_active'): + # Return a special marker so we know to skip module loading for top-level completion + index = self.INDEX[self._COMMAND_INDEX] + all_commands = list(index.keys()) + logger.debug("Top-level completion: %d commands available", len(all_commands)) + return '__top_level_completion__', all_commands # special marker, command list return None # Get the top-level command, like `network` in `network vnet create -h` From 912bff6d71d466447a3514772605d57de85c3e0d Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Mon, 5 Jan 2026 16:42:43 +1100 Subject: [PATCH 07/11] wip: add total time print statement --- src/azure-cli/azure/cli/__main__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/__main__.py b/src/azure-cli/azure/cli/__main__.py index 1a8b61fc84e..344ce187c20 100644 --- a/src/azure-cli/azure/cli/__main__.py +++ b/src/azure-cli/azure/cli/__main__.py @@ -11,8 +11,9 @@ import sys import os -# Track completion performance +# Track completion performance - store start time for argcomplete to use if os.environ.get('_ARGCOMPLETE'): + sys._cli_start_time = start_time print(f"[PERF] az CLI entry point at {start_time}", file=sys.stderr, flush=True) from azure.cli.core import telemetry @@ -67,6 +68,13 @@ def cli_main(cli, args): finally: # Log the invoke finish time invoke_finish_time = timeit.default_timer() + + if os.environ.get('_ARGCOMPLETE'): + total_time = invoke_finish_time - start_time + print(f"[PERF] ============================================", file=sys.stderr, flush=True) + print(f"[PERF] TOTAL TAB COMPLETION TIME: {total_time:.3f} seconds", file=sys.stderr, flush=True) + print(f"[PERF] ============================================", file=sys.stderr, flush=True) + logger.info("Command ran in %.3f seconds (init: %.3f, invoke: %.3f)", invoke_finish_time - start_time, init_finish_time - start_time, From b99f53f63ff8ca8a6bd70a0d27536ad6ea624127 Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Wed, 7 Jan 2026 14:50:19 +1100 Subject: [PATCH 08/11] test: add argcomplete test for top-level cmds --- .../azure/cli/core/tests/test_argcomplete.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py b/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py index 52a24af9b3c..1b0047f102b 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py @@ -64,3 +64,21 @@ def dummy_completor(*args, **kwargs): with open('argcomplete.out') as f: self.assertEqual(f.read(), 'dummystorage ') os.remove('argcomplete.out') + + def test_top_level_completion(self): + """Test that top-level completion (az [tab]) returns command names from index""" + import os + import sys + + if sys.platform == 'win32': + self.skipTest('Skip argcomplete test on Windows') + + run_cmd(['az'], env=self.argcomplete_env('az ', '3')) + with open('argcomplete.out') as f: + completions = f.read().split() + # Verify common top-level commands are present + self.assertIn('account', completions) + self.assertIn('vm', completions) + self.assertIn('network', completions) + self.assertIn('storage', completions) + os.remove('argcomplete.out') From e55f1dee121bf0d3bf1b54b1533c4e8eb20a041b Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Wed, 7 Jan 2026 15:09:34 +1100 Subject: [PATCH 09/11] fix: revert perf print statements --- src/azure-cli-core/azure/cli/core/__init__.py | 28 ------------------- .../azure/cli/core/commands/__init__.py | 10 ------- src/azure-cli/azure/cli/__main__.py | 21 +------------- 3 files changed, 1 insertion(+), 58 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index c959018c41b..2e196a6afbd 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -422,29 +422,20 @@ def _get_extension_suppressions(mod_loaders): self.command_table.clear() # Import announced breaking changes in azure.cli.core._breaking_change.py - import time - t1 = time.time() import_core_breaking_changes() - print(f"[PERF] Breaking changes import: {time.time()-t1:.3f}s", file=sys.stderr, flush=True) command_index = None # Set fallback=False to turn off command index in case of regression - t1 = time.time() use_command_index = self.cli_ctx.config.getboolean('core', 'use_command_index', fallback=True) - print(f"[PERF] use_command_index: {use_command_index}, args: {args}, lookup time: {time.time()-t1:.3f}s", file=sys.stderr, flush=True) if use_command_index: command_index = CommandIndex(self.cli_ctx) index_result = command_index.get(args) - print(f"[PERF] index_result: {index_result}", file=sys.stderr, flush=True) if index_result: index_modules, index_extensions = index_result # Special case for top-level completion - create minimal command groups if index_modules == '__top_level_completion__': - import time from azure.cli.core.commands import AzCliCommand - start_time = time.time() - print(f"[PERF] Top-level completion mode: creating {len(index_extensions)} command stubs", file=sys.stderr, flush=True) # index_extensions contains the command names, not extensions for cmd_name in index_extensions: # Create a minimal command entry for tab completion @@ -453,24 +444,14 @@ def _get_extension_suppressions(mod_loaders): self.command_table[cmd_name] = AzCliCommand( self, cmd_name, lambda: None ) - elapsed = time.time() - start_time - print(f"[PERF] Created command stubs in {elapsed:.3f} seconds", file=sys.stderr, flush=True) return self.command_table # Always load modules and extensions, because some of them (like those in # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core - import time - start_time = time.time() - print(f"[PERF] Loading modules from index: {index_modules}", file=sys.stderr, flush=True) _update_command_table_from_modules(args, index_modules) - elapsed_modules = time.time() - start_time - print(f"[PERF] Loaded modules in {elapsed_modules:.3f} seconds", file=sys.stderr, flush=True) # The index won't contain suppressed extensions - start_time = time.time() _update_command_table_from_extensions([], index_extensions) - elapsed_extensions = time.time() - start_time - print(f"[PERF] Loaded extensions in {elapsed_extensions:.3f} seconds", file=sys.stderr, flush=True) logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table)) from azure.cli.core.util import roughly_parse_command @@ -518,25 +499,16 @@ def _get_extension_suppressions(mod_loaders): # No module found from the index. Load all command modules and extensions logger.debug("Loading all modules and extensions") - t1 = time.time() - print(f"[PERF] Loading ALL modules (no index)", file=sys.stderr, flush=True) _update_command_table_from_modules(args) - elapsed_all_modules = time.time() - t1 - print(f"[PERF] Loaded ALL modules in {elapsed_all_modules:.3f} seconds", file=sys.stderr, flush=True) - t1 = time.time() ext_suppressions = _get_extension_suppressions(self.loaders) # We always load extensions even if the appropriate module has been loaded # as an extension could override the commands already loaded. _update_command_table_from_extensions(ext_suppressions) - elapsed_all_extensions = time.time() - t1 - print(f"[PERF] Loaded ALL extensions in {elapsed_all_extensions:.3f} seconds", file=sys.stderr, flush=True) logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table)) if use_command_index: - t1 = time.time() command_index.update(self.command_table) - print(f"[PERF] Command index update: {time.time()-t1:.3f}s", file=sys.stderr, flush=True) return self.command_table diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 57dabece368..23b97b89203 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -506,13 +506,6 @@ class AzCliCommandInvoker(CommandInvoker): # pylint: disable=too-many-statements,too-many-locals,too-many-branches def execute(self, args): - import time - import sys - import os - _start_time = time.time() - if os.environ.get('_ARGCOMPLETE'): - print(f"[PERF] Starting completion at {_start_time}", file=sys.stderr, flush=True) - from knack.events import (EVENT_INVOKER_PRE_CMD_TBL_CREATE, EVENT_INVOKER_POST_CMD_TBL_CREATE, EVENT_INVOKER_CMD_TBL_LOADED, EVENT_INVOKER_PRE_PARSE_ARGS, EVENT_INVOKER_POST_PARSE_ARGS, @@ -593,9 +586,6 @@ def execute(self, args): if args[0].lower() == 'help': args[0] = '--help' - if os.environ.get('_ARGCOMPLETE'): - elapsed = time.time() - _start_time - print(f"[PERF] About to enable autocomplete. Elapsed so far: {elapsed:.3f} seconds", file=sys.stderr, flush=True) self.parser.enable_autocomplete() self.cli_ctx.raise_event(EVENT_INVOKER_PRE_PARSE_ARGS, args=args) diff --git a/src/azure-cli/azure/cli/__main__.py b/src/azure-cli/azure/cli/__main__.py index 344ce187c20..30b4f0f0b1f 100644 --- a/src/azure-cli/azure/cli/__main__.py +++ b/src/azure-cli/azure/cli/__main__.py @@ -9,12 +9,6 @@ start_time = timeit.default_timer() import sys -import os - -# Track completion performance - store start time for argcomplete to use -if os.environ.get('_ARGCOMPLETE'): - sys._cli_start_time = start_time - print(f"[PERF] az CLI entry point at {start_time}", file=sys.stderr, flush=True) from azure.cli.core import telemetry from azure.cli.core import get_default_cli @@ -35,10 +29,6 @@ def cli_main(cli, args): az_cli = get_default_cli() -if os.environ.get('_ARGCOMPLETE'): - elapsed = timeit.default_timer() - start_time - print(f"[PERF] CLI initialized in {elapsed:.3f} seconds", file=sys.stderr, flush=True) - telemetry.set_application(az_cli, ARGCOMPLETE_ENV_NAME) # Log the init finish time @@ -48,9 +38,6 @@ def cli_main(cli, args): try: telemetry.start() - if os.environ.get('_ARGCOMPLETE'): - print(f"[PERF] Calling cli_main() at {timeit.default_timer():.3f}", file=sys.stderr, flush=True) - exit_code = cli_main(az_cli, sys.argv[1:]) if exit_code == 0: @@ -68,13 +55,7 @@ def cli_main(cli, args): finally: # Log the invoke finish time invoke_finish_time = timeit.default_timer() - - if os.environ.get('_ARGCOMPLETE'): - total_time = invoke_finish_time - start_time - print(f"[PERF] ============================================", file=sys.stderr, flush=True) - print(f"[PERF] TOTAL TAB COMPLETION TIME: {total_time:.3f} seconds", file=sys.stderr, flush=True) - print(f"[PERF] ============================================", file=sys.stderr, flush=True) - + logger.info("Command ran in %.3f seconds (init: %.3f, invoke: %.3f)", invoke_finish_time - start_time, init_finish_time - start_time, From 0f47ae252692b4fb77678da8a74c5c7fa967a6ca Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Wed, 7 Jan 2026 15:27:02 +1100 Subject: [PATCH 10/11] fix: cleanup --- src/azure-cli-core/azure/cli/core/__init__.py | 3 ++- src/azure-cli-core/azure/cli/core/commands/__init__.py | 1 - src/azure-cli/azure/cli/__main__.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 2e196a6afbd..83127ae3d2c 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -610,8 +610,9 @@ def get(self, args): return None # Make sure the top-level command is provided, like `az version`. - # For top-level completion (az [tab]), use a special marker to skip module loading + # Skip command index for `az` or `az --help`. if not args or args[0].startswith('-'): + # For top-level completion (az [tab]) if not args and self.cli_ctx.data.get('completer_active'): # Return a special marker so we know to skip module loading for top-level completion index = self.INDEX[self._COMMAND_INDEX] diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 23b97b89203..fb2a9a3dece 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -517,7 +517,6 @@ def execute(self, args): args = _pre_command_table_create(self.cli_ctx, args) self.cli_ctx.raise_event(EVENT_INVOKER_PRE_CMD_TBL_CREATE, args=args) - # @TODO: this is one bottleneck self.commands_loader.load_command_table(args) self.cli_ctx.raise_event(EVENT_INVOKER_PRE_CMD_TBL_TRUNCATE, load_cmd_tbl_func=self.commands_loader.load_command_table, args=args) diff --git a/src/azure-cli/azure/cli/__main__.py b/src/azure-cli/azure/cli/__main__.py index 30b4f0f0b1f..dd35d5a0345 100644 --- a/src/azure-cli/azure/cli/__main__.py +++ b/src/azure-cli/azure/cli/__main__.py @@ -55,7 +55,6 @@ def cli_main(cli, args): finally: # Log the invoke finish time invoke_finish_time = timeit.default_timer() - logger.info("Command ran in %.3f seconds (init: %.3f, invoke: %.3f)", invoke_finish_time - start_time, init_finish_time - start_time, From c12f9647b6e55acba87852afb7e6a0916c297f15 Mon Sep 17 00:00:00 2001 From: Daniel Languiller Date: Thu, 8 Jan 2026 14:42:40 +1100 Subject: [PATCH 11/11] refactor: remove blank lines --- .vscode/launch.json | 4 ++-- src/azure-cli-core/azure/cli/core/__init__.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 82544dba4de..5d00ab3b09d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -49,8 +49,8 @@ "cwd": "${workspaceFolder}", "env": { "_ARGCOMPLETE": "1", - "COMP_LINE": "az ", - "COMP_POINT": "3", + "COMP_LINE": "az vm create --", + "COMP_POINT": "18", "_ARGCOMPLETE_SUPPRESS_SPACE": "0", "_ARGCOMPLETE_IFS": "\n", "_ARGCOMPLETE_SHELL": "powershell", diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 83127ae3d2c..0c333c24a66 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -432,7 +432,6 @@ def _get_extension_suppressions(mod_loaders): index_result = command_index.get(args) if index_result: index_modules, index_extensions = index_result - # Special case for top-level completion - create minimal command groups if index_modules == '__top_level_completion__': from azure.cli.core.commands import AzCliCommand @@ -445,11 +444,11 @@ def _get_extension_suppressions(mod_loaders): self, cmd_name, lambda: None ) return self.command_table - + # Always load modules and extensions, because some of them (like those in # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core _update_command_table_from_modules(args, index_modules) - + # The index won't contain suppressed extensions _update_command_table_from_extensions([], index_extensions)