Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to PerfLens are recorded here. Format follows
uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html); pre-1.0
releases may break APIs between minor versions when needed.

## [Unreleased]

### Fixed

- **`perflens push-agent --help` prints usage.** It handed `--help` to ssh as
the host and failed with `ssh failed: unknown option -- -`. `-h`/`--help`
now print the usage and exit 0, and any other argument starting with `-` is
a usage error before ssh runs, rather than an option ssh or scp would parse.
- **`perflens serve --help` described `--token` backwards.** It said agents
must present the secret in their hello, which stopped being true in 0.10.0:
the server presents the pairing code to the agent through the `auth` command,
and the agent never sends one. The help now says so, and notes that a
pre-0.10.0 agent's hello token is still accepted with a warning.
- **`--max-samples` help understated memory.** It put the default's plateau
near 850 MB; the measured figure at 500000 samples is near 1.1 GB, as the
README and reference already said.
- **README release-assets table.** A sentence between table rows cut off the
`perflens-tools-linux-{x86_64,aarch64}.tar.gz` row, which rendered as plain
text.

## [0.11.0] — 2026-09-13

First hands-on validation on **big-endian** hardware — a big-endian ARMv7
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ Release assets published on every tagged release:
| `perflens-agent-linux-aarch64_be` | Agent — static binary, Linux aarch64 BE |
| `perflens-agent-linux-armv7` | Agent — static binary, Linux armv7 (32-bit LE) |
| `perflens-agent-linux-armeb` | Agent — static binary, Linux armv7 BE |
| `perflens-tools-linux-{x86_64,aarch64}.tar.gz` | Static addr2line+readelf for `perflens provision` |

The asset suffix is the normalized arch, not `uname -m`: a device reporting `armv7l` fetches `perflens-agent-linux-armv7`.
| `perflens-tools-linux-{x86_64,aarch64}.tar.gz` | Static addr2line+readelf for `perflens provision` |

### Option B — build the agent yourself

Expand Down
18 changes: 16 additions & 2 deletions src/perflens/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,23 @@ def _download(url, dest):
return False


_PUSH_AGENT_USAGE = """\
usage: perflens push-agent USER@HOST [PORT]

Detect the device arch over ssh, download the matching static agent binary
from the latest GitHub release, and scp it to ~/.perflens/bin on the device.
PORT is the ssh port (default 22).
"""


def _run_push_agent(argv):
if not argv:
print('usage: perflens push-agent USER@HOST [PORT]', file=sys.stderr)
if any(a in ('-h', '--help') for a in argv):
print(_PUSH_AGENT_USAGE, end='')
return 0
# ssh and scp would parse a leading '-' as one of their own options, so a
# flag in the host or port position is a usage mistake, never passed on.
if not argv or any(a.startswith('-') for a in argv):
print(_PUSH_AGENT_USAGE, end='', file=sys.stderr)
return 2
host = argv[0]
ssh_port = argv[1] if len(argv) > 1 else '22'
Expand Down
10 changes: 6 additions & 4 deletions src/perflens/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def config_from_args(argv=None):
parser.add_argument('--max-samples', type=int, default=500000,
help='Max accumulated samples before oldest are dropped '
'(default: 500000). Costs roughly 1.7 KB of RSS '
'per sample, so the default tops out near 850 MB '
'per sample, so the default plateaus near 1.1 GB '
'on a busy multi-threaded target; lower it if '
'that matters more than history depth')
parser.add_argument('--inline', action='store_true', default=True,
Expand All @@ -282,9 +282,11 @@ def config_from_args(argv=None):
'confined to (default: your home directory)')
parser.add_argument('--token', type=str,
default=os.environ.get('PERFLENS_TOKEN'),
help='Shared secret agents must present in their '
'hello (agents pass --token / PERFLENS_TOKEN); '
'connections without it are rejected')
help='Pairing code to present to the agent (or '
'PERFLENS_TOKEN); the wizard can supply one per '
'connection instead. A pre-0.10.0 agent that '
'sends it in its hello is still accepted, with '
'a warning')
parser.add_argument('--sessions-dir', type=str, default=None,
metavar='DIR',
help='Where to save profiling sessions '
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,40 @@ def test_import_passes_an_existing_file_through_to_serve(no_serve, tmp_path):
# push-agent
# ---------------------------------------------------------------------------

@pytest.fixture()
def no_ssh(monkeypatch):
"""Record subprocess.run calls instead of running ssh/scp."""
calls = []
monkeypatch.setattr(cli.subprocess, 'run',
lambda argv, **kwargs: calls.append(argv))
return calls


def test_push_agent_without_a_host_is_a_usage_error(capsys):
assert cli.main(['push-agent']) == 2
assert 'usage' in capsys.readouterr().err.lower()


@pytest.mark.parametrize('argv', [['--help'], ['-h'], ['user@host', '--help']])
def test_push_agent_help_prints_usage_without_running_ssh(capsys, no_ssh, argv):
"""`--help` used to reach ssh as the host and fail with
'ssh failed: unknown option -- -'."""
assert cli.main(['push-agent'] + argv) == 0
assert 'usage: perflens push-agent' in capsys.readouterr().out
assert no_ssh == []


@pytest.mark.parametrize('argv', [
['--port'],
['-oProxyCommand=true'],
['user@host', '-p'],
])
def test_push_agent_rejects_a_flag_as_host_or_port(capsys, no_ssh, argv):
assert cli.main(['push-agent'] + argv) == 2
assert 'usage' in capsys.readouterr().err.lower()
assert no_ssh == []


@pytest.mark.parametrize('machine,asset_arch', [
('x86_64', 'x86_64'),
('aarch64', 'aarch64'),
Expand Down
Loading