Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6dac56a
Add CodSpeed performance benchmarks for linter
codspeed-hq[bot] Apr 13, 2026
6c1acee
Remove -timeout flag from CodSpeed benchmark command
codspeed-hq[bot] Apr 13, 2026
5a2aa8d
chore: save
Apr 13, 2026
d57ace8
chore: save
Apr 13, 2026
c67f6fc
chore: save
Apr 13, 2026
399e4ec
chore: lint
Apr 13, 2026
afc05b7
chore: save
Apr 13, 2026
2a7cf75
chore: lint
Apr 13, 2026
0185062
chore: js benchmark
Apr 13, 2026
8d1f900
chore: lint
Apr 13, 2026
07bdebd
chore: update pnpm lock
Apr 13, 2026
ced8094
chore: update yml
Apr 13, 2026
36a1762
chore: fix codspeed version
Apr 13, 2026
3452dad
chore: update yml
simplyme0823 Apr 14, 2026
0ca6314
chore: save
simplyme0823 Apr 14, 2026
4255736
chore: save
simplyme0823 Apr 14, 2026
bae50fa
chore: save
simplyme0823 Apr 14, 2026
a2700b8
chore: use tinybench
simplyme0823 Apr 14, 2026
a441cdb
chore: use tsc
simplyme0823 Apr 14, 2026
9bc7656
chore: save
simplyme0823 Apr 14, 2026
68e03b5
chore: save
simplyme0823 Apr 14, 2026
119ca06
chore: save
simplyme0823 Apr 14, 2026
cbabb9f
chore: save
simplyme0823 Apr 14, 2026
910847a
chore: save
simplyme0823 Apr 14, 2026
eec4134
chore: save
simplyme0823 Apr 14, 2026
2ebe675
chore: save
simplyme0823 Apr 14, 2026
5736e0c
chore: fix ci
simplyme0823 Apr 14, 2026
b7f9f94
chore: save
simplyme0823 Apr 14, 2026
a09f5bb
chore: save
simplyme0823 Apr 14, 2026
6e25ba8
chore: update runners
Apr 14, 2026
4e3b823
chore: update runners
Apr 14, 2026
99f22a2
chore: update runners
Apr 14, 2026
a8576c2
chore: update runners
Apr 14, 2026
9fe2419
chore: update runners
Apr 14, 2026
c29271d
chore: update runners
Apr 14, 2026
bf3357b
fix: tinybench
Apr 14, 2026
bcab02f
fix: tinybench
Apr 14, 2026
959c5fb
chore: runner
Apr 15, 2026
65133d9
feat: add js time
Apr 15, 2026
3b95f97
feat: upload js time
Apr 15, 2026
abdc37a
feat: mode
Apr 15, 2026
113090b
chore: merge ci
Apr 15, 2026
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
95 changes: 95 additions & 0 deletions .github/actions/setup-codspeed/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Setup CodSpeed
description: Install the pinned CodSpeed runner with retry and hash verification

inputs:
runner-version:
description: CodSpeed runner version to install.
default: '4.13.0'
installer-sha256:
description: Expected SHA-256 for the installer script.
default: '55d150594275efb4a983b2f5626e84646269f42678f72554a26d6ce25b64e2d4'
download-retries:
description: Number of download attempts before failing.
default: '5'
retry-delay-seconds:
description: Delay between download attempts.
default: '3'

runs:
using: composite
steps:
- name: Install CodSpeed runner
shell: bash
run: |
set -euo pipefail

RUNNER_VERSION="${{ inputs.runner-version }}"
EXPECTED_HASH="${{ inputs.installer-sha256 }}"
DOWNLOAD_RETRIES="${{ inputs.download-retries }}"
RETRY_DELAY_SECONDS="${{ inputs.retry-delay-seconds }}"
INSTALLER_URL="https://codspeed.io/v${RUNNER_VERSION}/install.sh"
INSTALLER_TMP="$(mktemp)"
CURL_ERR="$(mktemp)"
trap 'rm -f "$INSTALLER_TMP" "$CURL_ERR"' EXIT

checksum() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}

attempt=1
while true; do
: >"$INSTALLER_TMP"
: >"$CURL_ERR"
CURL_EXIT=0
HTTP_CODE=""

if HTTP_CODE="$(curl -sSL -o "$INSTALLER_TMP" -w "%{http_code}" "$INSTALLER_URL" 2>"$CURL_ERR")"; then
CURL_EXIT=0
else
CURL_EXIT=$?
fi

if [ "$CURL_EXIT" -eq 0 ] && [ "$HTTP_CODE" -lt 400 ]; then
break
fi

if [ "$CURL_EXIT" -eq 0 ] && [ -n "$HTTP_CODE" ] && [ "$HTTP_CODE" -ge 400 ]; then
FAILURE_REASON="HTTP ${HTTP_CODE} - $(cat "$INSTALLER_TMP")"
else
FAILURE_REASON="$(cat "$CURL_ERR")"
fi

RETRYABLE_FAILURE=1
if [ "$CURL_EXIT" -eq 0 ]; then
case "$HTTP_CODE" in
408|429|500|502|503|504)
RETRYABLE_FAILURE=1
;;
*)
RETRYABLE_FAILURE=0
;;
esac
fi

if [ "$RETRYABLE_FAILURE" -eq 0 ] || [ "$attempt" -ge "$DOWNLOAD_RETRIES" ]; then
echo "::error title=Failed to install CodSpeed CLI::Installation of CodSpeed CLI with version ${RUNNER_VERSION} failed after ${DOWNLOAD_RETRIES} attempts.%0AReason: ${FAILURE_REASON}"
exit 1
fi

echo "CodSpeed installer download failed on attempt ${attempt}/${DOWNLOAD_RETRIES}: ${FAILURE_REASON}"
attempt=$((attempt + 1))
sleep "$RETRY_DELAY_SECONDS"
done

ACTUAL_HASH="$(checksum "$INSTALLER_TMP")"
if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then
echo "::error::Installer hash mismatch for version ${RUNNER_VERSION}. Expected: ${EXPECTED_HASH}, Got: ${ACTUAL_HASH}"
exit 1
fi
echo "Installer hash verified for version ${RUNNER_VERSION}"

bash "$INSTALLER_TMP" --quiet
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,71 @@ jobs:

- name: Run tests
run: cargo test --verbose

benchmark-go:
name: Run Go benchmarks
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: true
fetch-depth: 1

- name: Setup Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.26.0'
cache: false

- name: Run benchmarks
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2
with:
mode: walltime
run: go test -bench=. -benchtime=5s ./internal/linter/ ./cmd/rslint

benchmark-binding:
name: Run binding benchmarks
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: true
fetch-depth: 1

- name: Setup Go
uses: ./.github/actions/setup-go
with:
go-version: '1.26.0'
cache-name: benchmark-binding

- name: Setup Node.js
uses: ./.github/actions/setup-node

- name: Build JS package
run: pnpm --filter @rslint/core run build

- name: Run binding benchmarks
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2
timeout-minutes: 30
env:
RAYON_NUM_THREADS: 1
CODSPEED_EXPERIMENTAL_FAIR_SCHED: true
GH_MATRIX: '${{ toJson(matrix) }}'
GH_STRATEGY: '${{ toJson(strategy) }}'
with:
mode: walltime
run: pnpm run bench:binding
runner-version: 4.13.0
go-runner-version: 1.2.0

done:
needs:
- test-go
Expand All @@ -314,6 +379,8 @@ jobs:
- lint
- test-wasm
- test-rust
- benchmark-go
- benchmark-binding
if: always()
runs-on: ubuntu-latest
name: CI Done
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,12 @@ npm/tsgo/*/local
## vscode settings
.vscode/settings.json


# go cache
packages/rslint/pkg/

# rust
target/

# generated rule manifest
website/generated/
website/generated/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<a href="https://npmjs.com/package/@rslint/core?activeTab=readme"><img src="https://img.shields.io/npm/v/@rslint/core?style=flat-square&colorA=564341&colorB=EDED91" alt="npm version" /></a>
<a href="https://npmcharts.com/compare/@rslint/core?minimal=true"><img src="https://img.shields.io/npm/dm/@rslint/core.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="downloads" /></a>
<a href="https://github.com/web-infra-dev/rslint/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="license" /></a>
<a href="https://codspeed.io/web-infra-dev/rslint?utm_source=badge"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed"/></a>
</p>

> [!NOTE]
Expand Down
Binary file added [RFC] RSLint CI Benchmark .pdf
Binary file not shown.
22 changes: 22 additions & 0 deletions cmd/rslint/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"sync"

Expand Down Expand Up @@ -63,13 +64,34 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error)
// Create filesystem
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
allowedFiles := []string{}
allowedFileSet := map[string]struct{}{}

// Respect explicit file targeting from the JS API so benchmark and API
// callers can avoid relying on config glob traversal order.
for _, file := range req.Files {
absPath, err := filepath.Abs(file)
if err != nil {
return nil, fmt.Errorf("failed to resolve lint file %s: %w", file, err)
}
normalizedPath := tspath.NormalizePath(absPath)
if _, ok := allowedFileSet[normalizedPath]; ok {
continue
}
allowedFiles = append(allowedFiles, normalizedPath)
allowedFileSet[normalizedPath] = struct{}{}
}

// Apply file contents if provided
if len(req.FileContents) > 0 {
fileContents := make(map[string]string, len(req.FileContents))
for k, v := range req.FileContents {
normalizePath := tspath.NormalizePath(k)
fileContents[normalizePath] = v
if _, ok := allowedFileSet[normalizePath]; ok {
continue
}
allowedFiles = append(allowedFiles, normalizePath)
allowedFileSet[normalizePath] = struct{}{}
}
fs = utils.NewOverlayVFS(fs, fileContents)

Expand Down
74 changes: 74 additions & 0 deletions cmd/rslint/programs_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"os"
"path/filepath"
"strconv"
"testing"

"github.com/microsoft/typescript-go/shim/bundled"
"github.com/microsoft/typescript-go/shim/compiler"
"github.com/microsoft/typescript-go/shim/tspath"
"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
rslintconfig "github.com/web-infra-dev/rslint/internal/config"
"github.com/web-infra-dev/rslint/internal/utils"
)

var benchmarkWorkspaceOwnershipSink int

func BenchmarkWorkspaceOwnership(b *testing.B) {
rootDir := b.TempDir()
packageNames := []string{"pkg-a", "pkg-b", "pkg-c"}

programs := make([]*compiler.Program, 0, len(packageNames))
configMap := map[string]rslintconfig.RslintConfig{
tspath.NormalizePath(rootDir): nil,
}

for _, packageName := range packageNames {
configDir := filepath.Join(rootDir, "packages", packageName)
programs = append(programs, createWorkspaceBenchmarkProgram(b, configDir, 20))
normalizedDir := tspath.NormalizePath(configDir)
configMap[normalizedDir] = nil
}

b.ReportAllocs()
b.ResetTimer()

for b.Loop() {
fileOwner := buildFileOwnerMap(programs, configMap)
benchmarkWorkspaceOwnershipSink = len(fileOwner)
}
Comment thread
simplyme0823 marked this conversation as resolved.
}

func createWorkspaceBenchmarkProgram(b *testing.B, configDir string, fileCount int) *compiler.Program {
b.Helper()

srcDir := filepath.Join(configDir, "src")
if err := os.MkdirAll(srcDir, 0o755); err != nil {
b.Fatalf("failed to create workspace benchmark dir %s: %v", srcDir, err)
}

for i := range fileCount {
filePath := filepath.Join(srcDir, "file"+strconv.Itoa(i)+".ts")
content := "export const value" + strconv.Itoa(i) + " = " + strconv.Itoa(i) + ";\n"
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
b.Fatalf("failed to write %s: %v", filePath, err)
}
}

tsconfig := `{"compilerOptions":{"target":"esnext","module":"esnext"},"include":["src/**/*.ts"]}`
if err := os.WriteFile(filepath.Join(configDir, "tsconfig.json"), []byte(tsconfig), 0o644); err != nil {
b.Fatalf("failed to write tsconfig.json: %v", err)
}

fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
host := utils.CreateCompilerHost(configDir, fs)
program, err := utils.CreateProgram(true, fs, configDir, "tsconfig.json", host)
if err != nil {
b.Fatalf("failed to create program: %v", err)
}

return program
}
Loading
Loading