Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ fab config set debug_enabled true
# Set default capacity
fab config set default_capacity "My Capacity"

# Configure output sorting
fab config set output_item_sort_criteria bytype
# Configure output sorting for ls
fab ls ws1.Workspace --sort_by type

# View all settings
fab config ls
Expand Down
5 changes: 1 addition & 4 deletions docs/essentials/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ The Fabric CLI provides a comprehensive set of configuration settings that allow
| `debug_enabled` | Toggles additional diagnostic logs for troubleshooting | `BOOLEAN` | `false` |
| `context_persistence_enabled` | Persists CLI navigation context in command line mode across sessions | `BOOLEAN` | `false` |
| `encryption_fallback_enabled` | Permits storing tokens in plain text if secure encryption is unavailable | `BOOLEAN` | `false` |
| `job_cancel_ontimeout` | Cancels job runs that exceed the timeout period | `BOOLEAN` | `true` |
| `local_definition_labels` | Indicates the local JSON file path for label definitions mapping | `VARCHAR` | |
| `mode` | Determines the CLI mode (`interactive` or `command_line`) | `VARCHAR` | `command_line` |
| `output_item_sort_criteria` | Defines items output order (`byname` or `bytype`) | `VARCHAR` | `byname`|
| `show_hidden` | Displays all Fabric elements | `BOOLEAN` | `false` |
| `default_az_admin` | Defines the default Fabric administrator email for capacities | `VARCHAR` | |
| `default_az_location` | Defines the default Azure location for capacities | `VARCHAR` | |
| `default_az_resource_group` | Defines the default Azure resource group for capacities | `VARCHAR` | |
| `default_az_subscription_id` | Defines the default Azure subscription for capacities | `VARCHAR` | |
| `default_capacity` | Defines the default Fabric capacity for `mkdir` | `VARCHAR` | |
| `default_open_experience` | Defines the default Fabric portal user experience (`fabric` or `powerbi`) | `VARCHAR` | `fabric` |
| `output_format` | Defines the CLI output format (`text` or `json`) | `VARCHAR` | `text` |
| `folder_listing_enabled` | Enables recursive folder listing when using `ls` command | `BOOLEAN` | `false` |
| `workspace_private_links_enabled` | Enables workspace private links support in `api` command | `BOOLEAN` | `false` |
Expand All @@ -41,4 +38,4 @@ fab config get <setting_name>
fab config set <setting_name> <value>
```

For detailed examples, see the [Configuration Examples](../examples/config_examples.md).
For detailed examples, see the [Configuration Examples](../examples/config_examples.md).
28 changes: 1 addition & 27 deletions docs/examples/config_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,6 @@ Allow authentication token cache in plaintext when encryption is unavailable.
fab config set encryption_fallback_enabled true
```

### Enable/Disable Job Timeout Behavior

Control whether jobs are cancelled when they timeout.

```
fab config set job_cancel_ontimeout true
```

### Set Labels Definition File

Specify a local file containing label definitions for the CLI.
Expand All @@ -86,15 +78,6 @@ Set the default interaction mode for the CLI.
fab config set mode interactive
```

### Configure Item Sort Order

Set the default sorting criteria for listing items.
**Allowed Values:** `byname`, `bytype`

```
fab config set output_item_sort_criteria bytype
```

### Show Hidden Elements

Control visibility of hidden Fabric elements like virtual elements.
Expand Down Expand Up @@ -146,13 +129,4 @@ Configure the default Fabric capacity for workspace operations.
fab config set default_capacity cap1
```

### Set Default Open Experience

Configure the default experience when opening items in web browser.
**Allowed Values:** `powerbi`, `fabric`

```
fab config set default_open_experience powerbi
```

For complete details on all available configuration settings, see the [Settings documentation](../essentials/settings.md#available-settings).
For complete details on all available configuration settings, see the [Settings documentation](../essentials/settings.md#available-settings).
7 changes: 2 additions & 5 deletions docs/examples/job_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,11 @@ fab job run ws1.Workspace/sjd1.SparkJobDefinition --timeout 120

#### Run Job with Timeout (No Cancellation)

Configure the CLI to continue jobs even after timeout.
Use a command flag to continue jobs even after timeout.

```py
# Configure to not cancel jobs on timeout
fab config set job_cancel_ontimeout false

# Job will continue running even after timeout
fab job run ws1.Workspace/sjd1.SparkJobDefinition --timeout 120
fab job run ws1.Workspace/sjd1.SparkJobDefinition --timeout 120 --cancel_on_timeout false
```

### Asynchronous Job Execution
Expand Down
270 changes: 270 additions & 0 deletions scripts/benchmark_startup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
Benchmark CLI startup performance.

Compares the current branch against 'main' (or any other branch) by measuring:
1. Module import time (fabric_cli.main)
2. CLI invocation time (fab --version)
3. Heavy dependency loading (msal, jwt, cryptography, requests, prompt_toolkit)

Usage:
# Compare current branch against main
python scripts/benchmark_startup.py

# Compare current branch against a specific branch/tag/commit
python scripts/benchmark_startup.py --baseline v1.3.0

# Run only on the current branch (no git checkout)
python scripts/benchmark_startup.py --current-only

# Change number of iterations (default: 10)
python scripts/benchmark_startup.py --iterations 20
"""

import argparse
import importlib
import json
import os
import shutil
import statistics
import subprocess
import sys
import time


HEAVY_MODULES = ["msal", "jwt", "cryptography", "requests", "prompt_toolkit", "psutil"]


def measure_import_time(iterations: int) -> dict:
"""Measure fabric_cli.main import time across multiple iterations."""
times = []
for _ in range(iterations):
# Clear all fabric_cli modules from cache
mods = [k for k in sys.modules if k.startswith("fabric_cli")]
for m in mods:
del sys.modules[m]

start = time.perf_counter()
importlib.import_module("fabric_cli.main")
elapsed_ms = (time.perf_counter() - start) * 1000
times.append(elapsed_ms)

return {
"median_ms": round(statistics.median(times), 1),
"min_ms": round(min(times), 1),
"max_ms": round(max(times), 1),
"mean_ms": round(statistics.mean(times), 1),
"stdev_ms": round(statistics.stdev(times), 1) if len(times) > 1 else 0,
"samples": times,
}


def check_heavy_modules() -> dict:
"""Check which heavy modules are loaded after importing fabric_cli.main."""
# Clear all fabric_cli modules
mods = [k for k in sys.modules if k.startswith("fabric_cli")]
for m in mods:
del sys.modules[m]

# Also clear heavy modules
for mod in HEAVY_MODULES:
keys = [k for k in sys.modules if k.startswith(mod)]
for k in keys:
del sys.modules[k]

importlib.import_module("fabric_cli.main")

return {mod: mod in sys.modules for mod in HEAVY_MODULES}


def measure_cli_time(iterations: int) -> dict:
"""Measure 'fab --version' wall-clock time."""
fab_path = shutil.which("fab")
if not fab_path:
return {"error": "'fab' not found in PATH. Run 'pip install -e .' first."}

times = []
for _ in range(iterations):
start = time.perf_counter()
subprocess.run(
[fab_path, "--version"],
capture_output=True,
text=True,
)
elapsed_ms = (time.perf_counter() - start) * 1000
times.append(elapsed_ms)

return {
"median_ms": round(statistics.median(times), 1),
"min_ms": round(min(times), 1),
"max_ms": round(max(times), 1),
"mean_ms": round(statistics.mean(times), 1),
"stdev_ms": round(statistics.stdev(times), 1) if len(times) > 1 else 0,
}


def run_benchmark(label: str, iterations: int) -> dict:
"""Run all benchmarks and return results."""
print(f"\n{'=' * 60}")
print(f" Benchmarking: {label}")
print(f"{'=' * 60}")

# 1. Import time
print(f" Measuring import time ({iterations} iterations)...", end="", flush=True)
import_results = measure_import_time(iterations)
print(f" {import_results['median_ms']:.0f}ms median")

# 2. Heavy modules
print(" Checking heavy module loading...", end="", flush=True)
heavy_results = check_heavy_modules()
loaded = [m for m, v in heavy_results.items() if v]
print(f" {len(loaded)} loaded: {', '.join(loaded) if loaded else 'none'}")

# 3. CLI time
print(f" Measuring 'fab --version' ({iterations} iterations)...", end="", flush=True)
cli_results = measure_cli_time(iterations)
if "error" in cli_results:
print(f" {cli_results['error']}")
else:
print(f" {cli_results['median_ms']:.0f}ms median")

return {
"label": label,
"import_time": import_results,
"heavy_modules": heavy_results,
"cli_time": cli_results,
}


def print_comparison(baseline: dict, current: dict):
"""Print a formatted comparison table."""
print(f"\n{'=' * 60}")
print(" COMPARISON")
print(f"{'=' * 60}\n")

bl = baseline["import_time"]["median_ms"]
cu = current["import_time"]["median_ms"]
diff = bl - cu
pct = (diff / bl * 100) if bl > 0 else 0

print(f" {'Metric':<30} {'Baseline':>10} {'Current':>10} {'Change':>10}")
print(f" {'-' * 62}")
print(f" {'Import time (median):':<30} {bl:>9.0f}ms {cu:>9.0f}ms {diff:>+8.0f}ms")
print(f" {'Import improvement:':<30} {'':>10} {'':>10} {pct:>+8.0f}%")

if "error" not in baseline["cli_time"] and "error" not in current["cli_time"]:
bl_cli = baseline["cli_time"]["median_ms"]
cu_cli = current["cli_time"]["median_ms"]
cli_diff = bl_cli - cu_cli
cli_pct = (cli_diff / bl_cli * 100) if bl_cli > 0 else 0
print(f" {'CLI time (median):':<30} {bl_cli:>9.0f}ms {cu_cli:>9.0f}ms {cli_diff:>+8.0f}ms")
print(f" {'CLI improvement:':<30} {'':>10} {'':>10} {cli_pct:>+8.0f}%")

print(f"\n {'Heavy modules at startup:':<30}")
for mod in HEAVY_MODULES:
bl_loaded = "LOADED" if baseline["heavy_modules"].get(mod) else "deferred"
cu_loaded = "LOADED" if current["heavy_modules"].get(mod) else "deferred"
marker = " ✓" if cu_loaded == "deferred" and bl_loaded == "LOADED" else ""
print(f" {mod:<25} {bl_loaded:>10} {cu_loaded:>10}{marker}")

print()


def git_checkout_and_install(ref: str):
"""Checkout a git ref and reinstall the package."""
print(f"\n Switching to '{ref}'...")
subprocess.run(["git", "checkout", ref], capture_output=True, check=True)
subprocess.run(
[sys.executable, "-m", "pip", "install", "-e", ".", "-q"],
capture_output=True,
check=True,
)
# Clear all cached fabric_cli modules after reinstall
mods = [k for k in sys.modules if k.startswith("fabric_cli")]
for m in mods:
del sys.modules[m]


def main():
parser = argparse.ArgumentParser(
description="Benchmark CLI startup performance between branches."
)
parser.add_argument(
"--baseline",
default="main",
help="Git ref to compare against (default: main)",
)
parser.add_argument(
"--iterations", "-n",
type=int,
default=10,
help="Number of iterations per measurement (default: 10)",
)
parser.add_argument(
"--current-only",
action="store_true",
help="Only benchmark the current branch (skip baseline)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
args = parser.parse_args()

repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(repo_root)

# Get current branch name
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True,
)
current_branch = result.stdout.strip()

print(f" Repo: {repo_root}")
print(f" Current branch: {current_branch}")
print(f" Baseline: {args.baseline}")
print(f" Iterations: {args.iterations}")

results = {}

if not args.current_only:
# Benchmark baseline
try:
git_checkout_and_install(args.baseline)
results["baseline"] = run_benchmark(f"Baseline ({args.baseline})", args.iterations)
except subprocess.CalledProcessError:
print(f"\n ERROR: Could not checkout '{args.baseline}'. Does it exist?")
print(f" Try: python scripts/benchmark_startup.py --current-only")
sys.exit(1)
finally:
# Always return to original branch
subprocess.run(["git", "checkout", current_branch], capture_output=True)
subprocess.run(
[sys.executable, "-m", "pip", "install", "-e", ".", "-q"],
capture_output=True,
)
# Clear modules again
mods = [k for k in sys.modules if k.startswith("fabric_cli")]
for m in mods:
del sys.modules[m]

# Benchmark current
results["current"] = run_benchmark(f"Current ({current_branch})", args.iterations)

# Print comparison
if "baseline" in results:
print_comparison(results["baseline"], results["current"])

# JSON output
if args.json:
# Remove raw samples for cleaner JSON
for key in results:
if "samples" in results[key].get("import_time", {}):
del results[key]["import_time"]["samples"]
print(json.dumps(results, indent=2))


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion src/fabric_cli/commands/fs/fab_fs_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _add_claims_as_query_param(url: str, claims: Optional[dict[str, str]]) -> st

def _get_ux_experience() -> str:
if (
fab_state_config.get_config(fab_constant.FAB_DEFAULT_OPEN_EXPERIENCE)
fab_state_config.get_config(fab_constant.FAB_DEFAULT_OPEN_EXPERIENCE_DEPRECATED)
== "powerbi"
):
return fab_constant.FAB_DEFAULT_OPEN_EXPERIENCE_POWERBI
Expand Down
6 changes: 5 additions & 1 deletion src/fabric_cli/commands/fs/ls/fab_fs_ls_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ def exec(folder: Folder, args):
show_details = bool(args.long)

ws_elements: list[Union[Item, Folder]] = utils_fs.get_ws_elements(folder)
sort_elements = utils_fs.sort_ws_elements(ws_elements, show_details)
sort_elements = utils_fs.sort_ws_elements(
ws_elements,
show_details,
sort_criteria=getattr(args, "sort_by", None),
)

utils_ui.print_output_format(args, data=sort_elements, show_headers=show_details)
Loading