diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 26023fb..b26c25d 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -312,7 +312,7 @@ jobs: publish-npm: name: Publish to npm - needs: [build-macos-arm64, build-linux-x64, build-windows-x64] + needs: [build-macos-arm64, build-linux-x64] runs-on: ubuntu-latest if: | startsWith(github.ref, 'refs/tags/v') && diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e4d2c9..0fe5505 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,19 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' + + - name: Detect Blackfire credentials + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 + id: blackfire + run: | + if [ -n "${{ secrets.BLACKFIRE_SERVER_ID }}" ] && \ + [ -n "${{ secrets.BLACKFIRE_SERVER_TOKEN }}" ] && \ + [ -n "${{ secrets.BLACKFIRE_CLIENT_ID }}" ] && \ + [ -n "${{ secrets.BLACKFIRE_CLIENT_TOKEN }}" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + fi - name: Install dependencies (Linux) if: matrix.os == 'ubuntu-latest' @@ -472,6 +485,33 @@ jobs: - name: Run integration tests (macOS/Linux) if: matrix.os != 'windows-latest' run: npm run test:integration + + - name: Upload coverage reports to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 && always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + fail_ci_if_error: false + + - name: Setup Blackfire + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 && steps.blackfire.outputs.enabled == 'true' + uses: shivammathur/setup-php@v2 + with: + tools: blackfire + env: + BLACKFIRE_SERVER_ID: ${{ secrets.BLACKFIRE_SERVER_ID }} + BLACKFIRE_SERVER_TOKEN: ${{ secrets.BLACKFIRE_SERVER_TOKEN }} + BLACKFIRE_CLIENT_ID: ${{ secrets.BLACKFIRE_CLIENT_ID }} + BLACKFIRE_CLIENT_TOKEN: ${{ secrets.BLACKFIRE_CLIENT_TOKEN }} + + - name: Run Blackfire profile + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 && steps.blackfire.outputs.enabled == 'true' + run: blackfire run node scripts/blackfire-profile.js + + - name: Skip Blackfire profile + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 && steps.blackfire.outputs.enabled != 'true' + run: echo "Blackfire secrets are not configured; skipping Blackfire profiling." - name: Skip integration tests (Windows - FAISS not installed) if: matrix.os == 'windows-latest' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 27e47b4..91f2b4e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,7 +26,7 @@ jobs: - language: c-cpp build-mode: manual env: - FAISS_REF: 3f127ee3bad4922b1ba0cf2f17f1368d99a241ec + FAISS_REF: v1.14.1 steps: - name: Checkout code @@ -50,9 +50,8 @@ jobs: run: | sudo apt-get update sudo apt-get install -y cmake libopenblas-dev libomp-dev build-essential - git clone --depth 1 https://github.com/facebookresearch/faiss.git /tmp/faiss + git clone --branch "$FAISS_REF" --depth 1 https://github.com/facebookresearch/faiss.git /tmp/faiss cd /tmp/faiss - git checkout "$FAISS_REF" cmake -B build \ -DFAISS_ENABLE_GPU=OFF \ -DFAISS_ENABLE_PYTHON=OFF \ diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7d71789..9b1ea67 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -10,7 +10,7 @@ jobs: name: Nightly build, test, and example smoke runs-on: ubuntu-latest env: - FAISS_REF: 3f127ee3bad4922b1ba0cf2f17f1368d99a241ec + FAISS_REF: v1.14.1 OMP_NUM_THREADS: 1 OMP_THREAD_LIMIT: 1 @@ -31,9 +31,8 @@ jobs: - name: Build and install FAISS (CPU) run: | - git clone --depth 1 https://github.com/facebookresearch/faiss.git /tmp/faiss + git clone --branch "$FAISS_REF" --depth 1 https://github.com/facebookresearch/faiss.git /tmp/faiss cd /tmp/faiss - git checkout "$FAISS_REF" cmake -B build \ -DFAISS_ENABLE_GPU=OFF \ -DFAISS_ENABLE_PYTHON=OFF \ diff --git a/README.md b/README.md index a2b1e7a..517c30e 100644 --- a/README.md +++ b/README.md @@ -1045,6 +1045,24 @@ npm test # All tests npm run test:unit # Unit tests only npm run test:integration # Integration tests only npm run test:ci # CI tests (faster, no manual tests) +npm run profile:blackfire # Deterministic profiling workload used by Blackfire in CI +``` + +### CI Coverage And Profiling + +The GitHub Actions CI workflow uploads Jest coverage to Codecov from the `ubuntu-latest` / `Node 20` job. To enable it, add this repository secret: + +```bash +CODECOV_TOKEN +``` + +Blackfire profiling is also wired into that same canonical Linux CI job. The workflow automatically skips the Blackfire step when credentials are not configured. To enable Blackfire in GitHub Actions, add these repository secrets: + +```bash +BLACKFIRE_SERVER_ID +BLACKFIRE_SERVER_TOKEN +BLACKFIRE_CLIENT_ID +BLACKFIRE_CLIENT_TOKEN ``` ### Generating Documentation diff --git a/package.json b/package.json index 36d856c..d1a707a 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "test:ci": "jest --ci --coverage --config=jest.ci.config.js --testPathIgnorePatterns=manual", "test:unit": "jest test/unit", "test:integration": "jest test/integration", + "profile:blackfire": "node scripts/blackfire-profile.js", "test:watch": "jest --watch", "test:memory": "node --expose-gc test/memory.test.js", "test:local": "npm run clean && npm run build && npm test", diff --git a/scripts/blackfire-profile.js b/scripts/blackfire-profile.js new file mode 100644 index 0000000..d3fdb8a --- /dev/null +++ b/scripts/blackfire-profile.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +const { FaissIndex } = require('../src/js/index'); + +function createRng(seed) { + let state = seed >>> 0; + return function next() { + state = (Math.imul(1664525, state) + 1013904223) >>> 0; + return state / 0x100000000; + }; +} + +function generateNormalizedVectors(count, dims, seed) { + const next = createRng(seed); + const vectors = new Float32Array(count * dims); + + for (let row = 0; row < count; row++) { + let norm = 0; + const offset = row * dims; + + for (let col = 0; col < dims; col++) { + const value = next() * 2 - 1; + vectors[offset + col] = value; + norm += value * value; + } + + norm = Math.sqrt(norm) || 1; + for (let col = 0; col < dims; col++) { + vectors[offset + col] /= norm; + } + } + + return vectors; +} + +async function main() { + const dims = 128; + const vectorCount = 2048; + const queryCount = 32; + const iterations = 40; + + const index = new FaissIndex({ type: 'FLAT_L2', dims }); + + try { + const vectors = generateNormalizedVectors(vectorCount, dims, 12345); + const queries = generateNormalizedVectors(queryCount, dims, 67890); + + await index.add(vectors); + + for (let i = 0; i < iterations; i++) { + const queryOffset = (i % queryCount) * dims; + const query = queries.subarray(queryOffset, queryOffset + dims); + await index.search(query, 10); + } + + await index.searchBatch(queries, 10); + + const stats = index.getStats(); + console.log( + JSON.stringify({ + profiled: true, + type: stats.type, + dims: stats.dims, + ntotal: stats.ntotal, + iterations, + queryCount, + }) + ); + } finally { + index.dispose(); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});