fm-bench 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/docs/releasing.md +35 -0
- package/package.json +2 -2
- package/src/cli.js +14 -11
- package/src/system.js +65 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# fm-bench
|
|
2
2
|
|
|
3
|
+
[](https://github.com/devinoldenburg/fm-bench/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
Benchmark Apple's `fm` command on macOS 27+.
|
|
4
6
|
|
|
5
7
|
Measure latency, throughput, streaming smoothness, stability, and goodput across Apple Foundation Models — with repeatable prompt suites and JSON/CSV reports for automation.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
`fm-bench` uses semver tags (`v*.*.*`). Pushing a tag runs the **Release** workflow: test, lint, npm publish (with provenance), and GitHub release notes.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Repository secret **`NPM_TOKEN`**: npm automation token with publish access to `fm-bench`.
|
|
8
|
+
- **`main`** is green on CI.
|
|
9
|
+
|
|
10
|
+
## Option A — GitHub Actions (recommended)
|
|
11
|
+
|
|
12
|
+
1. Open **Actions → Version → Run workflow**.
|
|
13
|
+
2. Choose `patch`, `minor`, `major`, or an exact semver.
|
|
14
|
+
3. The job runs `npm version`, pushes the commit and tag to `main`.
|
|
15
|
+
4. The tag push triggers **Release** automatically.
|
|
16
|
+
|
|
17
|
+
## Option B — Local
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm ci && npm test && npm run lint
|
|
21
|
+
npm version patch # or minor / major
|
|
22
|
+
git push --follow-tags
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Re-run Release without republishing
|
|
26
|
+
|
|
27
|
+
If npm already has the version but GitHub release failed (or vice versa), use **Actions → Release → Run workflow** and enter the existing tag (for example `v0.5.3`). The workflow skips npm publish when that version is already on the registry and skips creating a duplicate GitHub release.
|
|
28
|
+
|
|
29
|
+
## Dry run
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm run publish:dry-run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
CI runs the same dry-run on every push to `main` and on pull requests.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fm-bench",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Dynamic benchmark CLI for Apple's fm command on macOS 27+.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"fm-bench": "
|
|
7
|
+
"fm-bench": "bin/fm-bench.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { loadHistory, renderHistoryReport } from './history.js';
|
|
|
6
6
|
import { runProcess } from './process.js';
|
|
7
7
|
import { createProgress } from './progress.js';
|
|
8
8
|
import { flattenResults, toCsv, writeReport } from './report.js';
|
|
9
|
+
import { parseBatteryOutput, parseThermalOutput } from './system.js';
|
|
9
10
|
import { legendEntries, renderBenchmarkReport, renderLatencyHistogram, renderLegend, renderModelsTable } from './table.js';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
@@ -473,21 +474,23 @@ async function runDoctor(options) {
|
|
|
473
474
|
}
|
|
474
475
|
|
|
475
476
|
const thermalResult = await runProcess('pmset', ['-g', 'therm'], { timeoutMs: 5_000 });
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const limit = Number.parseInt(thermalMatch[1], 10);
|
|
477
|
+
const thermal = parseThermalOutput(`${thermalResult.stdout || ''}${thermalResult.stderr || ''}`);
|
|
478
|
+
if (thermal.available && thermal.schedulerLimit != null) {
|
|
479
|
+
const limit = thermal.schedulerLimit;
|
|
480
480
|
checks.push(['thermal limit', `${limit}%`, limit >= 100]);
|
|
481
|
+
} else if (thermal.available && thermal.healthyIdle) {
|
|
482
|
+
checks.push(['thermal', 'no thermal pressure', true]);
|
|
483
|
+
} else if (!thermal.available) {
|
|
484
|
+
checks.push(['thermal', 'unavailable', true]);
|
|
481
485
|
}
|
|
482
486
|
|
|
483
487
|
const batteryResult = await runProcess('pmset', ['-g', 'batt'], { timeoutMs: 5_000 });
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
checks.push(['battery', `${pct}${charging ? ' (charging/AC)' : ' (battery)'}`, charging || Number.parseInt(pctMatch?.[1], 10) >= 20]);
|
|
488
|
+
const battery = parseBatteryOutput(`${batteryResult.stdout || ''}${batteryResult.stderr || ''}`);
|
|
489
|
+
if (battery.present) {
|
|
490
|
+
const pct = battery.pct != null ? `${battery.pct}%` : '?%';
|
|
491
|
+
const source = battery.onAC ? 'charging/AC' : 'battery';
|
|
492
|
+
const ok = battery.onAC || (battery.pct != null && battery.pct >= 20);
|
|
493
|
+
checks.push(['battery', `${pct} (${source})`, ok]);
|
|
491
494
|
}
|
|
492
495
|
|
|
493
496
|
const inspection = await inspectModels(options);
|
package/src/system.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Parsers for macOS system probes used by `fm-bench doctor`.
|
|
2
|
+
//
|
|
3
|
+
// macOS 27 changed the text emitted by `pmset -g therm` and `pmset -g batt`.
|
|
4
|
+
// These helpers accept the raw stdout/stderr of those commands and return
|
|
5
|
+
// structured values so the doctor command and its tests are not coupled to
|
|
6
|
+
// the exact wording of any one macOS release.
|
|
7
|
+
|
|
8
|
+
export function parseThermalOutput(output = '') {
|
|
9
|
+
const text = String(output).trim();
|
|
10
|
+
if (!text) {
|
|
11
|
+
return { available: false, schedulerLimit: null, raw: '' };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const limitMatch = text.match(/CPU_Scheduler_Limit\s*=\s*(-?\d+)/);
|
|
15
|
+
if (limitMatch) {
|
|
16
|
+
const schedulerLimit = Number.parseInt(limitMatch[1], 10);
|
|
17
|
+
return {
|
|
18
|
+
available: true,
|
|
19
|
+
schedulerLimit,
|
|
20
|
+
raw: text
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// macOS 27 (and recent releases) prints these informational "Note:" lines
|
|
25
|
+
// when the system is NOT under thermal pressure. Treat that as a healthy,
|
|
26
|
+
// explicitly-reported state rather than silently dropping the check.
|
|
27
|
+
const hasNote = /^Note:/m.test(text);
|
|
28
|
+
if (hasNote) {
|
|
29
|
+
return {
|
|
30
|
+
available: true,
|
|
31
|
+
schedulerLimit: null,
|
|
32
|
+
raw: text,
|
|
33
|
+
healthyIdle: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { available: false, schedulerLimit: null, raw: text };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseBatteryOutput(output = '') {
|
|
41
|
+
const text = String(output).trim();
|
|
42
|
+
if (!text) {
|
|
43
|
+
return { present: false, pct: null, onAC: false, raw: '' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = text.split(/\r?\n/);
|
|
47
|
+
const batteryLine = lines.find((line) => line.includes('%')) || '';
|
|
48
|
+
const pctMatch = batteryLine.match(/(\d+)%/);
|
|
49
|
+
const pct = pctMatch ? Number.parseInt(pctMatch[1], 10) : null;
|
|
50
|
+
|
|
51
|
+
// macOS 27 writes "Now drawing from 'AC Power'" on its own line and
|
|
52
|
+
// "AC attached" on the battery line. Older releases used "AC Power" in
|
|
53
|
+
// the battery line itself. Accept either form.
|
|
54
|
+
const onAC = /AC Power|AC attached|AC attached;/i.test(text)
|
|
55
|
+
|| /;\s*charging\b/i.test(batteryLine);
|
|
56
|
+
|
|
57
|
+
const present = /InternalBattery|present:\s*true/i.test(text);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
present: present || pct != null,
|
|
61
|
+
pct,
|
|
62
|
+
onAC,
|
|
63
|
+
raw: text
|
|
64
|
+
};
|
|
65
|
+
}
|