fm-bench 0.7.0 → 0.7.2
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/bin/fm-bench.js +16 -5
- package/package.json +8 -2
- package/src/bench.js +14 -5
- package/src/capabilities.js +3 -11
- package/src/fm.js +0 -16
- package/src/metrics.js +0 -9
package/bin/fm-bench.js
CHANGED
|
@@ -4,18 +4,29 @@ import { killActiveChildren } from '../src/process.js';
|
|
|
4
4
|
|
|
5
5
|
// Terminate any in-flight `fm` child process before exiting, then use the
|
|
6
6
|
// conventional 128 + signal exit code. A second signal exits immediately.
|
|
7
|
-
let
|
|
7
|
+
let signalsReceived = 0;
|
|
8
|
+
let interrupted = false;
|
|
8
9
|
for (const [signal, exitCode] of [['SIGINT', 130], ['SIGTERM', 143]]) {
|
|
9
10
|
process.on(signal, () => {
|
|
11
|
+
signalsReceived += 1;
|
|
12
|
+
interrupted = true;
|
|
13
|
+
// Carry the status on the process too, so the code survives even if the
|
|
14
|
+
// exit below is preempted.
|
|
15
|
+
process.exitCode = exitCode;
|
|
10
16
|
const killed = killActiveChildren(signal);
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
setTimeout(() => process.exit(exitCode),
|
|
17
|
+
if (signalsReceived > 1 || killed === 0) process.exit(exitCode);
|
|
18
|
+
// Deliberately not unref'd: this timer performs the exit after giving the
|
|
19
|
+
// killed children a moment to be reaped.
|
|
20
|
+
setTimeout(() => process.exit(exitCode), 500);
|
|
15
21
|
});
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
runCli(process.argv.slice(2)).catch((error) => {
|
|
25
|
+
if (interrupted) {
|
|
26
|
+
// The user asked to stop; a follow-up benchmark error is noise.
|
|
27
|
+
process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
19
30
|
const message = error?.message || String(error);
|
|
20
31
|
console.error(`fm-bench: ${message}`);
|
|
21
32
|
process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fm-bench",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Dynamic benchmark CLI for Apple's fm command on macOS 27+.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,14 +21,20 @@
|
|
|
21
21
|
"release:patch": "npm version patch && git push --follow-tags",
|
|
22
22
|
"release:minor": "npm version minor && git push --follow-tags",
|
|
23
23
|
"release:major": "npm version major && git push --follow-tags",
|
|
24
|
-
"publish:dry-run": "
|
|
24
|
+
"publish:dry-run": "node scripts/publish-dry-run.mjs",
|
|
25
25
|
"check:pack": "node scripts/check-package.mjs"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
28
|
"apple",
|
|
29
|
+
"apple-intelligence",
|
|
30
|
+
"apple-silicon",
|
|
29
31
|
"foundation-models",
|
|
30
32
|
"fm",
|
|
33
|
+
"llm",
|
|
31
34
|
"benchmark",
|
|
35
|
+
"benchmarking",
|
|
36
|
+
"latency",
|
|
37
|
+
"throughput",
|
|
32
38
|
"macos",
|
|
33
39
|
"cli"
|
|
34
40
|
],
|
package/src/bench.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { detectFmCapabilities } from './capabilities.js';
|
|
3
|
-
import { checkModelAvailability, collectEnvironment, countTokens, getQuotaUsage, respond } from './fm.js';
|
|
3
|
+
import { checkModelAvailability, collectEnvironment, countTokens, fmBinaryFromOptions, getQuotaUsage, respond } from './fm.js';
|
|
4
4
|
import { metricAvailability } from './metrics.js';
|
|
5
5
|
import { loadPrompts } from './prompts.js';
|
|
6
6
|
import { finalizeReportPayload } from './schema.js';
|
|
7
7
|
import { summarizeByModel } from './stats.js';
|
|
8
8
|
|
|
9
9
|
export async function inspectModels(options = {}) {
|
|
10
|
-
const fmBin = options
|
|
10
|
+
const fmBin = fmBinaryFromOptions(options);
|
|
11
11
|
const capabilities = options.capabilities ?? await detectFmCapabilities(fmBin, options);
|
|
12
12
|
const discovered = {
|
|
13
13
|
fmBin,
|
|
@@ -52,7 +52,7 @@ export async function inspectModels(options = {}) {
|
|
|
52
52
|
|
|
53
53
|
export async function runBenchmark(options = {}) {
|
|
54
54
|
const startedAt = new Date().toISOString();
|
|
55
|
-
const fmBin = options
|
|
55
|
+
const fmBin = fmBinaryFromOptions(options);
|
|
56
56
|
|
|
57
57
|
notify(options, { type: 'phase', phase: 'capabilities', message: 'probing fm capabilities' });
|
|
58
58
|
const capabilities = options.capabilities ?? await detectFmCapabilities(fmBin, options);
|
|
@@ -381,13 +381,22 @@ async function runSingleBenchmark(context) {
|
|
|
381
381
|
|
|
382
382
|
async function runLimited(items, concurrency, worker, options = {}) {
|
|
383
383
|
let nextIndex = 0;
|
|
384
|
+
// fail-fast stops admitting new work; calls already in flight are allowed to
|
|
385
|
+
// finish so no `fm` process is abandoned mid-run.
|
|
386
|
+
let cancelled = false;
|
|
384
387
|
const waitForSlot = createPacer(options.requestRate, options.rampUpMs);
|
|
385
388
|
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
386
|
-
while (nextIndex < items.length) {
|
|
389
|
+
while (!cancelled && nextIndex < items.length) {
|
|
387
390
|
const index = nextIndex;
|
|
388
391
|
nextIndex += 1;
|
|
389
392
|
await waitForSlot(index);
|
|
390
|
-
|
|
393
|
+
if (cancelled) break;
|
|
394
|
+
try {
|
|
395
|
+
await worker(items[index]);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
cancelled = true;
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
391
400
|
}
|
|
392
401
|
});
|
|
393
402
|
await Promise.all(workers);
|
package/src/capabilities.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
11
|
import { stripAnsi } from './ansi.js';
|
|
12
|
-
import { parseModelsFromHelp } from './fm-help.js';
|
|
12
|
+
import { parseAvailabilityList, parseModelsFromHelp } from './fm-help.js';
|
|
13
13
|
import { runProcess } from './process.js';
|
|
14
14
|
|
|
15
15
|
const SECTION_HEADER = /^\s*[A-Z][A-Z0-9 /-]+\s*$/;
|
|
@@ -179,16 +179,8 @@ async function discoverModelsFromAvailability(fmBin, options = {}) {
|
|
|
179
179
|
env: options.env ?? process.env
|
|
180
180
|
});
|
|
181
181
|
if (result.error) return [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for (const line of text.split(/\r?\n/)) {
|
|
185
|
-
const match = line.match(/^\s*([A-Za-z0-9._-]+)(?:\s+model)?\s+(?:is\s+)?(available|unavailable|not available)\b/i);
|
|
186
|
-
if (!match) continue;
|
|
187
|
-
const name = match[1].toLowerCase();
|
|
188
|
-
if (['the', 'no', 'model', 'system'].includes(name) && name !== 'system') continue;
|
|
189
|
-
models.set(name, { name, description: '' });
|
|
190
|
-
}
|
|
191
|
-
return [...models.values()];
|
|
182
|
+
return parseAvailabilityList(`${result.stdout}${result.stderr}`)
|
|
183
|
+
.map((model) => ({ name: model.name, description: '' }));
|
|
192
184
|
}
|
|
193
185
|
|
|
194
186
|
function escapeRegExp(value) {
|
package/src/fm.js
CHANGED
|
@@ -25,22 +25,6 @@ export async function getFmHelp(fmBin, timeoutMs = 10_000) {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Discover models, preferring an already-detected capability probe so a run
|
|
30
|
-
* does not spawn `fm --help` more than once.
|
|
31
|
-
* @param {Record<string, any>} options
|
|
32
|
-
*/
|
|
33
|
-
export async function discoverModels(options = {}) {
|
|
34
|
-
const fmBin = fmBinaryFromOptions(options);
|
|
35
|
-
const capabilities = options.capabilities ?? await detectFmCapabilities(fmBin, options);
|
|
36
|
-
return {
|
|
37
|
-
fmBin,
|
|
38
|
-
models: capabilities.models,
|
|
39
|
-
help: capabilities.help,
|
|
40
|
-
capabilities
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
28
|
/**
|
|
45
29
|
* Check one model against the detected `fm` build.
|
|
46
30
|
*
|
package/src/metrics.js
CHANGED
|
@@ -192,15 +192,6 @@ export function metricAvailability(capabilities = {}, options = {}) {
|
|
|
192
192
|
return metrics;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
export function metricDefinition(key) {
|
|
196
|
-
const definition = DEFINITIONS.find((item) => item.key === key);
|
|
197
|
-
return definition ? { ...definition, requires: [...definition.requires] } : null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function metricDefinitions() {
|
|
201
|
-
return DEFINITIONS.map((definition) => ({ ...definition, requires: [...definition.requires] }));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
195
|
/** Short human line for CLI output, e.g. "token counting: yes, quota: no". */
|
|
205
196
|
export function formatCapabilitySummary(capabilities = {}) {
|
|
206
197
|
const features = capabilities.features ?? {};
|