Browse Source
* wip * install hyperfine * switch sides * add permissions block * permissions * use ternary * wip wip wip * wip * other way? * force store * simplify * add more perms * add file size and units * change columns * add header * add hash * reverse the conditional to store * show last value for 0 * lastValue is now being sent * 0 * the return of all the build * Add min runsremotes/origin/alias-latest
![gal@spitfire.co.il](/assets/img/avatar_default.png)
![GitHub](/assets/img/avatar_default.png)
9 changed files with 458 additions and 6 deletions
@ -0,0 +1,6 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
eval "$(fnm env --multi)" |
||||||
|
fnm install v10.11.0 |
||||||
|
fnm use v10.11.0 |
||||||
|
node -v |
@ -0,0 +1,6 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
eval "$(~/.fnm-latest/fnm env --multi)" |
||||||
|
~/.fnm-latest/fnm install v10.11.0 |
||||||
|
~/.fnm-latest/fnm use v10.11.0 |
||||||
|
node -v |
@ -0,0 +1,6 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
eval "$(~/.fnm/fnm env --multi)" |
||||||
|
~/.fnm/fnm install v10.11.0 |
||||||
|
~/.fnm/fnm use v10.11.0 |
||||||
|
node -v |
@ -0,0 +1,8 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
export NVM_DIR="$HOME/.nvm" |
||||||
|
\. "$NVM_DIR/nvm.sh" # This loads nvm |
||||||
|
|
||||||
|
nvm install v10.11.0 |
||||||
|
nvm use v10.11.0 |
||||||
|
node -v |
@ -0,0 +1,36 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
export FNM_DIR |
||||||
|
|
||||||
|
BASE_DIR="$(dirname "$(realpath "$0")")" |
||||||
|
cd "$BASE_DIR" || exit 1 |
||||||
|
|
||||||
|
FNM_DIR="$(mktemp -d)" |
||||||
|
export PATH="$BASE_DIR/../target/release:$PATH" |
||||||
|
|
||||||
|
mkdir results 2>/dev/null || : |
||||||
|
|
||||||
|
if [ ! -f "$BASE_DIR/../target/release/fnm" ]; then |
||||||
|
echo "Can't access the release version of fnm.rs" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
if ! command -v hyperfine >/dev/null 2>&1; then |
||||||
|
echo "Can't access Hyperfine. Are you sure it is installed?" |
||||||
|
echo " if not, visit https://github.com/sharkdp/hyperfine" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Running it with warmup means we're going to have the versions |
||||||
|
# pre-installed. I think it is good because you open your shell more times |
||||||
|
# than you install Node versions. |
||||||
|
hyperfine \ |
||||||
|
--warmup=2 \ |
||||||
|
--min-runs=40 \ |
||||||
|
--time-unit=millisecond \ |
||||||
|
--export-json="./results/basic.json" \ |
||||||
|
--export-markdown="./results/basic.md" \ |
||||||
|
"basic/nvm" \ |
||||||
|
"basic/fnm" |
@ -0,0 +1,272 @@ |
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import z from "zod" |
||||||
|
import os from "node:os" |
||||||
|
import path from "node:path" |
||||||
|
import fetch from "node-fetch" |
||||||
|
import { execa } from "execa" |
||||||
|
import { binary, command, flag, option } from "cmd-ts" |
||||||
|
import Url from "cmd-ts/dist/cjs/batteries/url.js" |
||||||
|
import { run } from "cmd-ts" |
||||||
|
import fs from "node:fs/promises" |
||||||
|
import { dedent } from "ts-dedent" |
||||||
|
|
||||||
|
const HyperfineResult = z.object({ |
||||||
|
results: z.array( |
||||||
|
z.object({ |
||||||
|
command: z.string(), |
||||||
|
mean: z.number(), |
||||||
|
stddev: z.number(), |
||||||
|
median: z.number(), |
||||||
|
user: z.number(), |
||||||
|
system: z.number(), |
||||||
|
min: z.number(), |
||||||
|
max: z.number(), |
||||||
|
times: z.array(z.number()), |
||||||
|
exit_codes: z.array(z.literal(0)), |
||||||
|
}) |
||||||
|
), |
||||||
|
}) |
||||||
|
|
||||||
|
const BenchyResult = z.object({ |
||||||
|
data: z.object({ |
||||||
|
embed: z.object({ |
||||||
|
small: z.string(), |
||||||
|
big: z.string(), |
||||||
|
|
||||||
|
currentValue: z.number(), |
||||||
|
lastValue: z.number().optional(), |
||||||
|
diff: z |
||||||
|
.object({ |
||||||
|
value: z.number(), |
||||||
|
arrowImage: z.string(), |
||||||
|
}) |
||||||
|
.optional(), |
||||||
|
}), |
||||||
|
}), |
||||||
|
}) |
||||||
|
|
||||||
|
const { HttpUrl } = Url |
||||||
|
|
||||||
|
const cmd = command({ |
||||||
|
name: "run-benchmarks", |
||||||
|
args: { |
||||||
|
serverUrl: option({ |
||||||
|
long: "server-url", |
||||||
|
type: HttpUrl, |
||||||
|
defaultValue: () => new URL("https://benchy.hagever.com"), |
||||||
|
defaultValueIsSerializable: true, |
||||||
|
}), |
||||||
|
githubToken: option({ |
||||||
|
long: "github-token", |
||||||
|
env: "GITHUB_TOKEN", |
||||||
|
}), |
||||||
|
shouldStore: flag({ |
||||||
|
long: "store", |
||||||
|
}), |
||||||
|
}, |
||||||
|
async handler({ serverUrl, githubToken, shouldStore }) { |
||||||
|
const repoName = "fnm" |
||||||
|
const repoOwner = "schniz" |
||||||
|
|
||||||
|
const hyperfineResult = await runHyperfine() |
||||||
|
|
||||||
|
if (!hyperfineResult.success) { |
||||||
|
console.error( |
||||||
|
`Can't run benchmarks: wrong data:`, |
||||||
|
hyperfineResult.error.issues |
||||||
|
) |
||||||
|
process.exitCode = 1 |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const { results } = hyperfineResult.data |
||||||
|
|
||||||
|
const url = new URL("/api/metrics", serverUrl) |
||||||
|
const trackedKeys = ["median", "max", "mean", "min", "stddev"] |
||||||
|
|
||||||
|
const metrics = results |
||||||
|
.flatMap((result) => { |
||||||
|
return trackedKeys.map((key) => { |
||||||
|
return { |
||||||
|
displayName: `${result.command}/${key}`, |
||||||
|
value: result[key] * 1000, // everything is in seconds
|
||||||
|
units: "ms", |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
.concat([ |
||||||
|
{ |
||||||
|
displayName: `binary size`, |
||||||
|
value: await getFilesize(), |
||||||
|
units: "kb", |
||||||
|
}, |
||||||
|
]) |
||||||
|
.map((metric) => { |
||||||
|
return { |
||||||
|
...metric, |
||||||
|
key: `${os.platform()}/${os.arch()}/${metric.displayName}`, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const embeds$ = metrics.map(async ({ key, value, displayName, units }) => { |
||||||
|
const response = await fetch(String(url), { |
||||||
|
method: "PUT", |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/json", |
||||||
|
}, |
||||||
|
body: JSON.stringify({ |
||||||
|
coloring: "lower-is-better", |
||||||
|
repoOwner, |
||||||
|
repoName, |
||||||
|
githubToken, |
||||||
|
key, |
||||||
|
value, |
||||||
|
}), |
||||||
|
}) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Response is not okay: ${response.status}`) |
||||||
|
} |
||||||
|
|
||||||
|
const { data } = BenchyResult.parse(await response.json()) |
||||||
|
return { |
||||||
|
displayName, |
||||||
|
units, |
||||||
|
...data.embed, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const embeds = await Promise.all(embeds$) |
||||||
|
|
||||||
|
const table = (() => { |
||||||
|
const rows = embeds |
||||||
|
.map((data) => { |
||||||
|
return dedent` |
||||||
|
<tr> |
||||||
|
<td><code>${data.displayName}</code></td> |
||||||
|
<td><code>${round(data.currentValue, 2)}${data.units}</code></td> |
||||||
|
<td>${ |
||||||
|
typeof data.lastValue === "undefined" |
||||||
|
? "" |
||||||
|
: `<code>${round(data.lastValue, 2)}${data.units}</code>` |
||||||
|
}</td> |
||||||
|
<td>${ |
||||||
|
!data.diff |
||||||
|
? "<code>0</code>" |
||||||
|
: dedent` |
||||||
|
<picture title=${JSON.stringify( |
||||||
|
data.diff.value > 0 ? "increase" : "decrease" |
||||||
|
)}> |
||||||
|
<img width="16" valign="middle" src="${ |
||||||
|
data.diff.arrowImage |
||||||
|
}"> |
||||||
|
</picture> |
||||||
|
<code>${data.diff.value > 0 ? "+" : ""}${round( |
||||||
|
data.diff.value, |
||||||
|
2 |
||||||
|
)}${data.units}</code> |
||||||
|
` |
||||||
|
}</td> |
||||||
|
<td> |
||||||
|
<details><summary><img valign="middle" src="${ |
||||||
|
data.small |
||||||
|
}" /></summary><br/><img src="${data.big}" /></details> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
` |
||||||
|
}) |
||||||
|
.join("\n") |
||||||
|
return dedent` |
||||||
|
<table> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th align="left">benchmark</th> |
||||||
|
<th>current value</th> |
||||||
|
<th>last value</th> |
||||||
|
<th>diff</th> |
||||||
|
<th>trend</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
${rows} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
` |
||||||
|
})() |
||||||
|
|
||||||
|
console.log(table) |
||||||
|
|
||||||
|
if (shouldStore) { |
||||||
|
const response = await fetch(String(url), { |
||||||
|
method: "POST", |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/json", |
||||||
|
}, |
||||||
|
body: JSON.stringify({ |
||||||
|
repoOwner, |
||||||
|
repoName, |
||||||
|
githubToken, |
||||||
|
metrics, |
||||||
|
}), |
||||||
|
}) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Response is not okay: ${response.status}`) |
||||||
|
} |
||||||
|
|
||||||
|
console.error(await response.json()) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {number} number |
||||||
|
* @param {number} digits |
||||||
|
*/ |
||||||
|
function round(number, digits) { |
||||||
|
const pow = Math.pow(10, digits) |
||||||
|
return Math.round(number * pow) / pow |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the size of the `fnm` binary in kilobytes |
||||||
|
* |
||||||
|
* @returns number |
||||||
|
*/ |
||||||
|
async function getFilesize() { |
||||||
|
const fnmBinary = await execa("which", ["fnm"]) |
||||||
|
const stat = await fs.stat(fnmBinary.stdout.trim()) |
||||||
|
return Math.round(stat.size / 1024) |
||||||
|
} |
||||||
|
|
||||||
|
async function runHyperfine() { |
||||||
|
const file = path.join(os.tmpdir(), `bench-${Date.now()}.json`) |
||||||
|
await execa( |
||||||
|
`hyperfine`, |
||||||
|
[ |
||||||
|
"--min-runs=20", |
||||||
|
`--export-json=${file}`, |
||||||
|
"--warmup=2", |
||||||
|
...[ |
||||||
|
"--command-name=fnm_basic", |
||||||
|
new URL("./basic/fnm", import.meta.url).pathname, |
||||||
|
], |
||||||
|
// ...[
|
||||||
|
// "--command-name=nvm_basic",
|
||||||
|
// new URL("./basic/nvm", import.meta.url).pathname,
|
||||||
|
// ],
|
||||||
|
], |
||||||
|
{ |
||||||
|
stdout: process.stderr, |
||||||
|
stderr: process.stderr, |
||||||
|
stdin: "ignore", |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
const json = JSON.parse(await fs.readFile(file, "utf8")) |
||||||
|
const parsed = HyperfineResult.safeParse(json) |
||||||
|
return parsed |
||||||
|
} |
||||||
|
|
||||||
|
run(binary(cmd), process.argv) |
Loading…
Reference in new issue