diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 01dd57a..0ec3c49 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -106,7 +106,7 @@ jobs: run_install: false - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: 'pnpm' - name: Get pnpm store directory id: pnpm-cache @@ -140,7 +140,7 @@ jobs: run_install: false - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: 'pnpm' - name: Get pnpm store directory id: pnpm-cache @@ -176,7 +176,7 @@ jobs: # run_install: false # - uses: actions/setup-node@v3 # with: - # node-version: 16.x + # node-version: 18.x # cache: 'pnpm' # - name: Get pnpm store directory # id: pnpm-cache @@ -212,7 +212,7 @@ jobs: run_install: false - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: 'pnpm' - name: Get pnpm store directory id: pnpm-cache @@ -340,7 +340,7 @@ jobs: run_install: false - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: 'pnpm' - name: Get pnpm store directory id: pnpm-cache @@ -357,3 +357,72 @@ jobs: - name: Generate command markdown run: | pnpm run generate-command-docs --check --binary-path=$(which fnm) + + run_e2e_benchmarks: + runs-on: ubuntu-latest + name: bench/linux + needs: [build_static_linux_binary] + permissions: + contents: write + pull-requests: write + steps: + - name: install necessary shells + run: sudo apt-get update && sudo apt-get install -y fish zsh bash hyperfine + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: fnm-linux + path: target/release + - name: mark binary as executable + run: chmod +x target/release/fnm + - name: install fnm as binary + run: | + sudo install target/release/fnm /bin + fnm --version + - uses: pnpm/action-setup@v2.2.4 + with: + run_install: false + - uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'pnpm' + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - run: pnpm install + - name: Run benchmarks + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHOULD_STORE: ${{ toJson(!github.event.pull_request) }} + id: benchmark + run: | + delimiter="$(openssl rand -hex 8)" + echo "markdown<<${delimiter}" >> "${GITHUB_OUTPUT}" + node benchmarks/run.mjs --store=$SHOULD_STORE >> "${GITHUB_OUTPUT}" + echo "${delimiter}" >> "${GITHUB_OUTPUT}" + + - name: Create a PR comment + if: ${{ github.event.pull_request }} + uses: thollander/actions-comment-pull-request@v1 + with: + message: | + + ## Linux Benchmarks for ${{ github.event.pull_request.head.sha }} + ${{ steps.benchmark.outputs.markdown }} + comment_includes: '' + + - name: Create a commit comment + if: ${{ !github.event.pull_request }} + uses: peter-evans/commit-comment@v2 + with: + body: | + ## Linux Benchmarks + ${{ steps.benchmark.outputs.markdown }} diff --git a/benchmarks/basic/fnm b/benchmarks/basic/fnm new file mode 100755 index 0000000..3f25cfc --- /dev/null +++ b/benchmarks/basic/fnm @@ -0,0 +1,6 @@ +#!/bin/bash + +eval "$(fnm env --multi)" +fnm install v10.11.0 +fnm use v10.11.0 +node -v diff --git a/benchmarks/basic/fnm_latest_master b/benchmarks/basic/fnm_latest_master new file mode 100755 index 0000000..c3f0f68 --- /dev/null +++ b/benchmarks/basic/fnm_latest_master @@ -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 diff --git a/benchmarks/basic/fnm_reason b/benchmarks/basic/fnm_reason new file mode 100755 index 0000000..7833821 --- /dev/null +++ b/benchmarks/basic/fnm_reason @@ -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 diff --git a/benchmarks/basic/nvm b/benchmarks/basic/nvm new file mode 100755 index 0000000..daf10a6 --- /dev/null +++ b/benchmarks/basic/nvm @@ -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 diff --git a/benchmarks/run b/benchmarks/run new file mode 100755 index 0000000..20314e0 --- /dev/null +++ b/benchmarks/run @@ -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" diff --git a/benchmarks/run.mjs b/benchmarks/run.mjs new file mode 100644 index 0000000..f5b29b1 --- /dev/null +++ b/benchmarks/run.mjs @@ -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` +
${data.displayName}
${round(data.currentValue, 2)}${data.units}
${round(data.lastValue, 2)}${data.units}
`
+ }0
"
+ : dedent`
+ ${data.diff.value > 0 ? "+" : ""}${round(
+ data.diff.value,
+ 2
+ )}${data.units}
+ `
+ }benchmark | +current value | +last value | +diff | +trend | +
---|