diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..972d5cd --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,37 @@ +name: Publish package to GitHub Packages + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js & GitHub Packages registry + uses: actions/setup-node@v4 + with: + node-version: '18.x' + registry-url: 'https://npm.pkg.github.com' + + - name: Install, build, & test + run: | + npm install --legacy-peer-deps + npm run build + npm test + env: + NPM_TOKEN: ${{secrets.NPM_TOKEN}} + + - name: Publish to GitHub Packages + run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + NPM_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/roll20-update-benchmark.yml b/.github/workflows/roll20-update-benchmark.yml new file mode 100644 index 0000000..79033dd --- /dev/null +++ b/.github/workflows/roll20-update-benchmark.yml @@ -0,0 +1,63 @@ +name: Publish benchmarks to GitHub Pages + +on: + push: + branches: + - master + workflow_dispatch: + schedule: + - cron: '0 0 */14 * *' # every 14 days at midnight UTC + +permissions: + contents: read + pages: write + id-token: write + +env: + BENCHMARKS_URL: https://roll20.github.io/detect-gpu + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install & build + run: | + npm install --legacy-peer-deps + npm run build + + - name: Update benchmarks + run: npm run update-benchmarks + + - name: Validate *local* benchmark JSON shape + run: | + npx jest test/validate-benchmarks-shape.test.ts \ + --env=jsdom \ + --runTestsByPath + env: + BENCHMARKS_URL: file://${{ github.workspace }}/benchmarks + + - name: Upload benchmarks as Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: benchmarks + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9663c7..c256a40 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Run actions on Pull Request or Push +name: Run tests and linting on pull requests on: push: branches: @@ -14,13 +14,18 @@ on: jobs: test: - name: Run test & lint - runs-on: Ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + # make sure we don't pick up our repo's .npmrc + - name: Un-configure GitHub Packages + run: rm -f .npmrc + - uses: actions/setup-node@v4 with: node-version: '18.x' + - uses: actions/cache@v4 id: yarn-cache with: @@ -28,6 +33,7 @@ jobs: node_modules */*/node_modules key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }} + - run: yarn install if: ${{ steps.yarn-cache.outputs.cache-hit != 'true' }} diff --git a/.github/workflows/update-benchmark.yml b/.github/workflows/update-benchmark.yml deleted file mode 100644 index 60c2210..0000000 --- a/.github/workflows/update-benchmark.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Weekly update -on: - schedule: - - cron: "0 0 * * 0" - workflow_dispatch: - -jobs: - auto-update: - name: Automated benchmarks update - runs-on: Ubuntu-20.04 - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - uses: actions/setup-node@v4 - with: - node-version: '18.x' - - uses: actions/cache@v4 - id: yarn-cache - with: - path: | - node_modules - */*/node_modules - key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }} - - run: yarn install - if: ${{ steps.yarn-cache.outputs.cache-hit != 'true' }} - - - run: yarn install --prefers-offline - if: ${{ steps.yarn-cache.outputs.cache-hit == 'true' }} - - - name: Bump patch - run: npm version patch --no-git-tag-version - - - name: Update - id: update - run: yarn run update-benchmarks - - - name: Build final result - run: yarn build - - - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.NPM_TOKEN }} - access: public - - - name: Push Changes - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Automated weekly update of the benchmark. - skip_dirty_check: true - skip_fetch: true diff --git a/.npmrc b/.npmrc index 43c97e7..95dc053 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -package-lock=false +@roll20:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${NPM_TOKEN} diff --git a/package.json b/package.json index 22ffa92..86e63ce 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,27 @@ { - "name": "detect-gpu", - "version": "5.0.70", + "name": "@roll20/detect-gpu", + "version": "5.0.74", "description": "Classify GPU's based on their benchmark score in order to provide an adaptive experience.", "author": "Tim van Scherpenzeel", "license": "MIT", "main": "dist/detect-gpu.umd.js", "module": "dist/detect-gpu.esm.js", "types": "dist/src/index.d.ts", - "homepage": "https://github.com/pmndrs/detect-gpu#readme", + "homepage": "https://github.com/Roll20/detect-gpu#readme", "bugs": { - "url": "https://github.com/pmndrs/detect-gpu/issues" + "url": "https://github.com/Roll20/detect-gpu/issues" }, "repository": { "type": "git", - "url": "https://github.com/pmndrs/detect-gpu.git" + "url": "https://github.com/Roll20/detect-gpu.git" }, "files": [ "dist" ], - "keywords": [ - "gpu", - "detect", - "webgl", - "webgl2", - "three.js", - "babylonjs", - "three", - "babylon", - "3d", - "typescript", - "javascript" - ], + "publishConfig": { + "@roll20:registry": "https://npm.pkg.github.com" + }, + "keywords": [], "scripts": { "start": "rollup -c rollup/config.lib.ts -w --configPlugin rollup-plugin-typescript2", "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" \"test/**/*.test.ts\" \"rollup/**/*.ts\" \"scripts/**/*.ts\" \"scripts/**/*.js\" --fix --cache --cache-location ~/.eslintcache/eslintcache", @@ -42,7 +33,8 @@ "build": "rollup -c rollup/config.lib.ts --configPlugin rollup-plugin-typescript2", "example": "rollup -w -c rollup/config.dev.ts --configPlugin rollup-plugin-typescript2", "parse-analytics": "node ./scripts/analytics_parser.js", - "update-benchmarks": "rimraf benchmarks && mkdir -p benchmarks && mkdir -p benchmarks-min && ts-node -O '{\"module\":\"commonjs\"}' ./scripts/update_benchmarks.ts && tar -czvf benchmarks.tar.gz benchmarks-min/*.json && rm -rf benchmarks-min" + "update-benchmarks": "rimraf benchmarks && mkdir -p benchmarks && mkdir -p benchmarks-min && ts-node -O '{\"module\":\"commonjs\"}' ./scripts/update_benchmarks.ts && tar -czvf benchmarks.tar.gz benchmarks-min/*.json && rm -rf benchmarks-min", + "prepare": "npm run build" }, "dependencies": { "webgl-constants": "^1.1.1" @@ -53,6 +45,7 @@ "@types/jest": "^29.5.3", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", + "cross-fetch": "^4.1.0", "csvtojson": "^2.0.10", "eslint": "^8.4.1", "eslint-config-prettier": "^9.0.0", diff --git a/scripts/update_benchmarks.ts b/scripts/update_benchmarks.ts index 76ef4cb..7d0f542 100755 --- a/scripts/update_benchmarks.ts +++ b/scripts/update_benchmarks.ts @@ -184,7 +184,13 @@ type Optional = Pick, K> & Omit; }); async function fetchBenchmarks() { - const browser = await puppeteer.launch({ headless: true }); + const browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + ], + }); const page = await browser.newPage(); await page.goto(BENCHMARK_URL, { waitUntil: 'networkidle2' }); diff --git a/src/index.ts b/src/index.ts index 2fb43c2..4bbaedc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -216,20 +216,23 @@ export const getGPUTier = async ({ } const tokenizedRenderer = tokenizeForLevenshteinDistance(renderer); - // eslint-disable-next-line prefer-const - let [gpu, , , , fpsesByPixelCount] = + + // pick the best matching row + const bestMatch = matchCount > 1 ? matched - .map( - (match) => - [ - match, - getLevenshteinDistance(tokenizedRenderer, match[2]), - ] as const + .map((match) => + [match, getLevenshteinDistance(tokenizedRenderer, match[2])] as const ) .sort(([, a], [, b]) => a - b)[0][0] : matched[0]; + // gpu name is always at index 0 + const gpu = bestMatch[0]; + + // fpses array is always the last element + const fpsesByPixelCount = bestMatch[bestMatch.length - 1] as ModelEntryScreen[]; + debug?.( `${renderer} matched closest to ${gpu} with the following screen sizes`, JSON.stringify(fpsesByPixelCount) diff --git a/src/internal/deobfuscateAppleGPU.ts b/src/internal/deobfuscateAppleGPU.ts index 9c06c3f..c2b2e80 100644 --- a/src/internal/deobfuscateAppleGPU.ts +++ b/src/internal/deobfuscateAppleGPU.ts @@ -56,6 +56,7 @@ export function deobfuscateAppleGPU( ['a15', codeA, 15], // ipad mini 6th gen / ipad 10th gen ['m1', codeA, 15], // ipad pro 11 5nd gen / ipad pro 12.9 5th gen / ipad air 5th gen ['m2', codeA, 15], // ipad pro 11 6nd gen / ipad pro 12.9 6th gen + ['m4', codeA, 15], // ipad pro 11 7nd gen / ipad pro 13 7th gen ] : [ // ['a4', 7], // 4 / ipod touch 4th gen @@ -72,6 +73,7 @@ export function deobfuscateAppleGPU( ['a15', codeA, 15], // 13 / 13 mini / 13 pro / 13 pro max / se 3rd gen / 14 / 14 plus ['a16', codeA, 15], // 14 pro / 14 pro max / 15 / 15 plus ['a17', codeA, 15], // 15 pro / 15 pro max + ['a18', codeA, 15], // 16 / 16 plus / 16 pro / 16 pro max ]; let chipsets: typeof possibleChipsets; diff --git a/test/validate-benchmarks-shape.test.ts b/test/validate-benchmarks-shape.test.ts new file mode 100644 index 0000000..91dbc7b --- /dev/null +++ b/test/validate-benchmarks-shape.test.ts @@ -0,0 +1,63 @@ +/** + * @jest-environment node + * + */ + +import fetch from 'cross-fetch'; +(global as any).fetch = fetch; +import fs from 'fs/promises'; + +const BENCHMARKS_URL = process.env.BENCHMARKS_URL || 'https://roll20.github.io/detect-gpu'; +const TEST_FILE = 'd-apple.json'; + +interface ScreenEntry extends Array { + 0: number; + 1: number; + 2: number; +} + +describe('Live benchmark data shape', () => { + let data: unknown; + + beforeAll(async () => { + const base = BENCHMARKS_URL.replace(/\/$/, ''); + if (base.startsWith('file://')) { + const dirPath = base.replace(/^file:\/\//, ''); + const raw = await fs.readFile(`${dirPath}/${TEST_FILE}`, 'utf8'); + data = JSON.parse(raw); + } else { + const res = await fetch(`${base}/${TEST_FILE}`); + expect(res.ok).toBe(true); + data = await res.json(); + } + }); + + it('is an array with a version string at index 0', () => { + expect(Array.isArray(data)).toBe(true); + expect(typeof (data as any)[0]).toBe('string'); + }); + + it('each entry has the correct tuple shape', () => { + const entries = (data as any).slice(1); + expect(entries.length).toBeGreaterThan(0); + + for (const entry of entries) { + expect(Array.isArray(entry)).toBe(true); + expect([4, 5]).toContain(entry.length); + + const screens = entry[entry.length - 1]; + expect(Array.isArray(screens)).toBe(true); + expect(screens.length).toBeGreaterThan(0); + + for (const screen of screens) { + expect(Array.isArray(screen)).toBe(true); + expect((screen as ScreenEntry).length).toBe(3); + + const [w, h, fps] = screen as ScreenEntry; + expect(typeof w).toBe('number'); + expect(typeof h).toBe('number'); + expect(typeof fps).toBe('number'); + } + } + }); +});