diff --git a/.eslintignore b/.eslintignore index d6126e3..b7bc7f0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ lib examples +benchmark/*.js diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..503f43d --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,126 @@ +name: Benchmark + +on: + pull_request: + branches: + - master + +permissions: + contents: read + pull-requests: write + +jobs: + benchmark: + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build project + run: yarn build + + - name: Run PR benchmarks + id: pr-benchmark + continue-on-error: true + run: | + echo "Running benchmarks on PR branch..." + yarn benchmark > pr-benchmark.txt 2>&1 + EXIT_CODE=$? + cat pr-benchmark.txt + exit $EXIT_CODE + + - name: Check PR benchmark status + if: steps.pr-benchmark.outcome == 'failure' + run: echo "āš ļø PR benchmarks failed - will attempt to extract partial results" + + - name: Extract PR results + id: pr-results + run: node benchmark/extract-results.js pr-benchmark.txt pr-results.json + + - name: Save benchmark scripts + run: | + mkdir -p /tmp/benchmark-scripts + cp benchmark/extract-results.js /tmp/benchmark-scripts/ + cp benchmark/compare-results.js /tmp/benchmark-scripts/ + + - name: Checkout base branch + run: | + git fetch origin ${{ github.event.pull_request.base.ref }} + git checkout origin/${{ github.event.pull_request.base.ref }} + + - name: Install dependencies (base) + run: yarn install --frozen-lockfile + + - name: Build project (base) + run: yarn build + + - name: Run base benchmarks + id: base-benchmark + continue-on-error: true + run: | + echo "Running benchmarks on base branch..." + yarn benchmark > base-benchmark.txt 2>&1 + EXIT_CODE=$? + cat base-benchmark.txt + exit $EXIT_CODE + + - name: Check base benchmark status + if: steps.base-benchmark.outcome == 'failure' + run: echo "āš ļø Base benchmarks failed - will attempt to extract partial results" + + - name: Extract base results + id: base-results + run: node /tmp/benchmark-scripts/extract-results.js base-benchmark.txt base-results.json + + - name: Compare and format results + id: compare + run: node /tmp/benchmark-scripts/compare-results.js base-results.json pr-results.json > comment.txt + + - name: Post comment to PR + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const comment = fs.readFileSync('comment.txt', 'utf8'); + + // Find existing benchmark comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const benchmarkComment = comments.find(comment => + comment.body.includes('šŸ“Š Benchmark Results') + ); + + if (benchmarkComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: benchmarkComment.id, + body: comment + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } diff --git a/.gitignore b/.gitignore index 2096cdb..b9bec56 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ lib yarn-error.log package-lock.json coverage +*-benchmark.txt +*-results.json +comment.txt diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..072494c --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,101 @@ +# Benchmarks + +This directory contains performance benchmarks for node-casbin. + +## Running Benchmarks Locally + +To run the benchmarks locally: + +```bash +yarn benchmark +``` + +This will: +1. Build the CommonJS version of the library +2. Build the benchmark suite +3. Run all benchmarks and display results + +## What is Benchmarked + +The benchmark suite tests the performance of: + +### RBAC Model +- `enforce()` (async) - both allow and deny cases +- `enforceSync()` - both allow and deny cases +- `getRolesForUser()` - get user roles +- `hasRoleForUser()` - check if user has a specific role + +### ABAC Model +- `enforce()` (async) - attribute-based access control +- `enforceSync()` - attribute-based access control + +### Basic Model +- `enforce()` (async) - basic access control +- `enforceSync()` - basic access control + +### Policy Management +- `getPolicy()` - retrieve all policies +- `hasPolicy()` - check if policy exists +- `getFilteredPolicy()` - retrieve filtered policies + +## Automated Benchmarking + +The benchmark workflow automatically runs on every Pull Request: + +1. **Runs benchmarks on PR branch** - measures performance of proposed changes +2. **Runs benchmarks on base branch** - establishes baseline performance +3. **Compares results** - calculates percentage changes +4. **Posts comment to PR** - displays results in an easy-to-read table + +### Understanding Benchmark Results + +The PR comment will show: +- **šŸš€** - Significant improvement (>5%) +- **āœ…** - Improvement (0-5%) +- **āž–** - No significant change +- **ā¬‡ļø** - Minor regression (0-5%) +- **āš ļø** - Regression (>5%) + +### What to Do About Regressions + +If your PR shows performance regressions: + +1. **Review the changes** - identify what might cause the slowdown +2. **Profile the code** - use Node.js profiling tools to find bottlenecks +3. **Consider alternatives** - can the same functionality be achieved more efficiently? +4. **Document trade-offs** - if the regression is unavoidable, document why the change is necessary + +Small regressions (<5%) are generally acceptable if: +- The change adds important functionality +- The change improves code maintainability +- The change fixes a bug or security issue + +## Adding New Benchmarks + +To add new benchmarks, edit `benchmark/benchmark.ts`: + +```typescript +// Create a new suite +const mySuite = createSuite('My Feature'); + +mySuite + .add('My benchmark', { + defer: true, // for async tests + fn: async (deferred: Benchmark.Deferred) => { + await myFunction(); + deferred.resolve(); + }, + }) + .on('complete', () => { + resolve(); + }) + .run({ async: true }); +``` + +For synchronous tests, omit the `defer` option: + +```typescript +mySuite.add('My sync benchmark', () => { + mySyncFunction(); +}); +``` diff --git a/benchmark/benchmark.ts b/benchmark/benchmark.ts new file mode 100644 index 0000000..2467a66 --- /dev/null +++ b/benchmark/benchmark.ts @@ -0,0 +1,258 @@ +// Copyright 2018 The Casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Benchmark from 'benchmark'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Use CommonJS require to import from built library +// eslint-disable-next-line @typescript-eslint/no-var-requires +const casbin = require('../cjs'); +const { newEnforcer } = casbin; + +interface BenchmarkResult { + name: string; + ops: number; + margin: number; + samples: number; +} + +const results: BenchmarkResult[] = []; + +// Verify required example files exist +function checkExampleFiles(): void { + const requiredFiles = [ + 'examples/rbac_model.conf', + 'examples/rbac_policy.csv', + 'examples/abac_model.conf', + 'examples/basic_model.conf', + 'examples/basic_policy.csv', + ]; + + const missingFiles: string[] = []; + for (const file of requiredFiles) { + if (!fs.existsSync(path.join(process.cwd(), file))) { + missingFiles.push(file); + } + } + + if (missingFiles.length > 0) { + console.error('Error: Required example files not found:'); + missingFiles.forEach((file) => console.error(` - ${file}`)); + console.error('\nPlease ensure you are running this from the repository root directory.'); + process.exit(1); + } +} + +async function setupEnforcers(): Promise<{ + rbacEnforcer: any; + abacEnforcer: any; + basicEnforcer: any; +}> { + const rbacEnforcer = await newEnforcer('examples/rbac_model.conf', 'examples/rbac_policy.csv'); + const abacEnforcer = await newEnforcer('examples/abac_model.conf'); + const basicEnforcer = await newEnforcer('examples/basic_model.conf', 'examples/basic_policy.csv'); + + return { rbacEnforcer, abacEnforcer, basicEnforcer }; +} + +function createSuite(name: string): Benchmark.Suite { + const suite = new Benchmark.Suite(name); + + suite.on('cycle', (event: Benchmark.Event) => { + const benchmark = event.target; + console.log(String(benchmark)); + + results.push({ + name: benchmark.name || 'Unknown', + ops: benchmark.hz || 0, + margin: benchmark.stats ? benchmark.stats.rme : 0, + samples: benchmark.stats ? benchmark.stats.sample.length : 0, + }); + }); + + suite.on('error', (event: Benchmark.Event) => { + console.error('Error in benchmark:', event.target); + }); + + return suite; +} + +async function runBenchmarks(): Promise { + // Check that all required files exist + checkExampleFiles(); + + console.log('Setting up enforcers...'); + const { rbacEnforcer, abacEnforcer, basicEnforcer } = await setupEnforcers(); + + console.log('\n--- Starting Benchmarks ---\n'); + + // RBAC Model Benchmarks + console.log('RBAC Model Benchmarks:'); + await new Promise((resolve) => { + const rbacSuite = createSuite('RBAC'); + + rbacSuite + .add('RBAC - enforce (allow)', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await rbacEnforcer.enforce('alice', 'data1', 'read'); + deferred.resolve(); + }, + }) + .add('RBAC - enforce (deny)', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await rbacEnforcer.enforce('bob', 'data2', 'write'); + deferred.resolve(); + }, + }) + .add('RBAC - enforceSync (allow)', () => { + rbacEnforcer.enforceSync('alice', 'data1', 'read'); + }) + .add('RBAC - enforceSync (deny)', () => { + rbacEnforcer.enforceSync('bob', 'data2', 'write'); + }) + .add('RBAC - getRolesForUser', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await rbacEnforcer.getRolesForUser('alice'); + deferred.resolve(); + }, + }) + .add('RBAC - hasRoleForUser', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await rbacEnforcer.hasRoleForUser('alice', 'data2_admin'); + deferred.resolve(); + }, + }) + .on('complete', () => { + resolve(); + }) + .run({ async: true }); + }); + + // ABAC Model Benchmarks + console.log('\nABAC Model Benchmarks:'); + await new Promise((resolve) => { + const abacSuite = createSuite('ABAC'); + + abacSuite + .add('ABAC - enforce (allow)', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await abacEnforcer.enforce({ name: 'alice', age: 16 }, '/data1', 'read'); + deferred.resolve(); + }, + }) + .add('ABAC - enforce (deny)', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await abacEnforcer.enforce({ name: 'bob', age: 30 }, '/data1', 'read'); + deferred.resolve(); + }, + }) + .add('ABAC - enforceSync (allow)', () => { + abacEnforcer.enforceSync({ name: 'alice', age: 16 }, '/data1', 'read'); + }) + .add('ABAC - enforceSync (deny)', () => { + abacEnforcer.enforceSync({ name: 'bob', age: 30 }, '/data1', 'read'); + }) + .on('complete', () => { + resolve(); + }) + .run({ async: true }); + }); + + // Basic Model Benchmarks + console.log('\nBasic Model Benchmarks:'); + await new Promise((resolve) => { + const basicSuite = createSuite('Basic'); + + basicSuite + .add('Basic - enforce (allow)', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await basicEnforcer.enforce('alice', 'data1', 'read'); + deferred.resolve(); + }, + }) + .add('Basic - enforce (deny)', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await basicEnforcer.enforce('alice', 'data1', 'write'); + deferred.resolve(); + }, + }) + .add('Basic - enforceSync (allow)', () => { + basicEnforcer.enforceSync('alice', 'data1', 'read'); + }) + .add('Basic - enforceSync (deny)', () => { + basicEnforcer.enforceSync('alice', 'data1', 'write'); + }) + .on('complete', () => { + resolve(); + }) + .run({ async: true }); + }); + + // Policy Management Benchmarks + console.log('\nPolicy Management Benchmarks:'); + await new Promise((resolve) => { + const policyEnforcer = basicEnforcer; + const policySuite = createSuite('Policy Management'); + + policySuite + .add('getPolicy', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await policyEnforcer.getPolicy(); + deferred.resolve(); + }, + }) + .add('hasPolicy', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await policyEnforcer.hasPolicy('alice', 'data1', 'read'); + deferred.resolve(); + }, + }) + .add('getFilteredPolicy', { + defer: true, + fn: async (deferred: Benchmark.Deferred) => { + await policyEnforcer.getFilteredPolicy(0, 'alice'); + deferred.resolve(); + }, + }) + .on('complete', () => { + resolve(); + }) + .run({ async: true }); + }); + + // Output results as JSON for CI + console.log('\n--- Benchmark Results (JSON) ---'); + console.log(JSON.stringify(results, null, 2)); +} + +runBenchmarks() + .then(() => { + console.log('\n--- Benchmarks Complete ---'); + process.exit(0); + }) + .catch((error) => { + console.error('Benchmark error:', error); + process.exit(1); + }); diff --git a/benchmark/compare-results.js b/benchmark/compare-results.js new file mode 100755 index 0000000..a0484ef --- /dev/null +++ b/benchmark/compare-results.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +// Script to compare benchmark results and generate a PR comment + +const fs = require('fs'); + +if (process.argv.length < 4) { + console.error('Usage: node compare-results.js '); + process.exit(1); +} + +const baseFile = process.argv[2]; +const prFile = process.argv[3]; + +function formatNumber(num) { + return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(num); +} + +function getPercentageChange(base, pr) { + if (!base || base === 0) return 'N/A'; + const change = ((pr - base) / base) * 100; + return change.toFixed(2); +} + +function getChangeEmoji(change) { + if (change === 'N/A') return 'āž–'; + const num = parseFloat(change); + if (num > 5) return 'šŸš€'; + if (num > 0) return 'āœ…'; + if (num < -5) return 'āš ļø'; + if (num < 0) return 'ā¬‡ļø'; + return 'āž–'; +} + +try { + const baseResults = JSON.parse(fs.readFileSync(baseFile, 'utf8')); + const prResults = JSON.parse(fs.readFileSync(prFile, 'utf8')); + + let comment = '## šŸ“Š Benchmark Results\n\n'; + comment += 'Performance comparison between base branch and PR:\n\n'; + comment += '| Benchmark | Base (ops/sec) | PR (ops/sec) | Change | |\n'; + comment += '|-----------|----------------|--------------|--------|---|\n'; + + // Create a map of base results + const baseMap = new Map(baseResults.map((r) => [r.name, r])); + + if (prResults.length === 0) { + comment += '\nāš ļø No benchmark results found for PR branch.\n'; + } else { + prResults.forEach((pr) => { + const base = baseMap.get(pr.name); + const baseOps = base ? base.ops : 0; + const change = getPercentageChange(baseOps, pr.ops); + const emoji = getChangeEmoji(change); + + comment += `| ${pr.name} | ${formatNumber(baseOps)} | ${formatNumber(pr.ops)} | ${change}% | ${emoji} |\n`; + }); + } + + comment += '\n---\n'; + comment += '**Legend:**\n'; + comment += '- šŸš€ Significant improvement (>5%)\n'; + comment += '- āœ… Improvement (0-5%)\n'; + comment += '- āž– No significant change\n'; + comment += '- ā¬‡ļø Minor regression (0-5%)\n'; + comment += '- āš ļø Regression (>5%)\n'; + + console.log(comment); +} catch (error) { + console.error('Error comparing benchmark results:', error.message); + const fallbackComment = + '## šŸ“Š Benchmark Results\n\nāš ļø Failed to generate benchmark comparison. Please check the workflow logs for details.\n'; + console.log(fallbackComment); + process.exit(0); +} diff --git a/benchmark/extract-results.js b/benchmark/extract-results.js new file mode 100755 index 0000000..0eb0e92 --- /dev/null +++ b/benchmark/extract-results.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +// Script to extract JSON benchmark results from benchmark output + +const fs = require('fs'); + +if (process.argv.length < 4) { + console.error('Usage: node extract-results.js '); + process.exit(1); +} + +const inputFile = process.argv[2]; +const outputFile = process.argv[3]; + +try { + const output = fs.readFileSync(inputFile, 'utf8'); + const jsonMatch = output.match(/--- Benchmark Results \(JSON\) ---\s*\n([\s\S]*?)\n--- Benchmarks Complete ---/); + + if (jsonMatch && jsonMatch[1]) { + const results = JSON.parse(jsonMatch[1]); + fs.writeFileSync(outputFile, JSON.stringify(results, null, 2)); + console.log(`Successfully extracted ${results.length} benchmark results to ${outputFile}`); + } else { + console.warn('Could not extract JSON results from output, writing empty array'); + fs.writeFileSync(outputFile, '[]'); + } +} catch (error) { + console.error('Error extracting benchmark results:', error.message); + // Write empty array on error to prevent workflow failure + fs.writeFileSync(outputFile, '[]'); + process.exit(0); +} diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json new file mode 100644 index 0000000..4172628 --- /dev/null +++ b/benchmark/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2017", + "outDir": "../lib/benchmark", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": false, + "moduleResolution": "node", + "baseUrl": "../lib" + }, + "include": ["*.ts"] +} diff --git a/package.json b/package.json index 64bd126..805b339 100644 --- a/package.json +++ b/package.json @@ -11,24 +11,28 @@ "build": "run-s clean && run-p build:*", "build:cjs": "tsc -p tsconfig.cjs.json", "build:esm": "tsc -p tsconfig.esm.json", + "build:benchmark": "tsc -p benchmark/tsconfig.json", "test": "jest", "lint": "eslint . --ext .js,.ts", "fmt": "eslint . --ext .js,.ts --fix", "semantic-release": "semantic-release", "commit": "git-cz", "clean": "rimraf lib", - "coverage": "jest --coverage" + "coverage": "jest --coverage", + "benchmark": "npm run build:cjs && npm run build:benchmark && node lib/benchmark/benchmark.js" }, "devDependencies": { "@semantic-release/commit-analyzer": "^8.0.1", "@semantic-release/github": "^7.2.3", "@semantic-release/npm": "^7.1.3", "@semantic-release/release-notes-generator": "^9.0.3", + "@types/benchmark": "^2.1.5", "@types/jest": "^26.0.20", "@types/node": "^10.5.3", "@types/picomatch": "^2.2.2", "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", + "benchmark": "^2.1.4", "coveralls": "^3.0.2", "cz-conventional-changelog": "^3.2.0", "eslint": "^7.22.0", diff --git a/yarn.lock b/yarn.lock index 96f8828..f9aeb1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -815,16 +815,6 @@ dependencies: any-observable "^0.3.0" -"@semantic-release/changelog@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-5.0.1.tgz#50a84b63e5d391b7debfe021421589fa2bcdafe4" - integrity sha512-unvqHo5jk4dvAf2nZ3aw4imrlwQ2I50eVVvq9D47Qc3R+keNqepx1vDYwkjF8guFXnOYaYcR28yrZWno1hFbiw== - dependencies: - "@semantic-release/error" "^2.1.0" - aggregate-error "^3.0.0" - fs-extra "^9.0.0" - lodash "^4.17.4" - "@semantic-release/commit-analyzer@^8.0.0", "@semantic-release/commit-analyzer@^8.0.1": version "8.0.1" resolved "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-8.0.1.tgz#5d2a37cd5a3312da0e3ac05b1ca348bf60b90bca" @@ -838,25 +828,11 @@ lodash "^4.17.4" micromatch "^4.0.2" -"@semantic-release/error@^2.1.0", "@semantic-release/error@^2.2.0": +"@semantic-release/error@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@semantic-release/error/-/error-2.2.0.tgz#ee9d5a09c9969eade1ec864776aeda5c5cddbbf0" integrity sha512-9Tj/qn+y2j+sjCI3Jd+qseGtHjOAeg7dU2/lVcqIQ9TV3QDaDXDYXcoOHU+7o2Hwh8L8ymL4gfuO7KxDs3q2zg== -"@semantic-release/git@^9.0.0": - version "9.0.0" - resolved "https://registry.npmjs.org/@semantic-release/git/-/git-9.0.0.tgz#304c4883c87d095b1faaae93300f1f1e0466e9a5" - integrity sha512-AZ4Zha5NAPAciIJH3ipzw/WU9qLAn8ENaoVAhD6srRPxTpTzuV3NhNh14rcAo8Paj9dO+5u4rTKcpetOBluYVw== - dependencies: - "@semantic-release/error" "^2.1.0" - aggregate-error "^3.0.0" - debug "^4.0.0" - dir-glob "^3.0.0" - execa "^4.0.0" - lodash "^4.17.4" - micromatch "^4.0.0" - p-reduce "^2.0.0" - "@semantic-release/github@^7.0.0": version "7.2.1" resolved "https://registry.npmjs.org/@semantic-release/github/-/github-7.2.1.tgz#e833245746413e0830b65112331ca0a00b35cf95" @@ -1023,6 +999,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/benchmark@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/benchmark/-/benchmark-2.1.5.tgz#940c1850c18fdfdaee3fd6ed29cd92ae0d445b45" + integrity sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1616,6 +1597,14 @@ before-after-hook@^2.2.0: resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz#73540563558687586b52ed217dad6a802ab1549c" integrity sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw== +benchmark@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ== + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + bin-links@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/bin-links/-/bin-links-2.2.1.tgz#347d9dbb48f7d60e6c11fe68b77a424bee14d61b" @@ -5230,7 +5219,7 @@ micromatch@^3.0.4, micromatch@^3.1.4, micromatch@^3.1.8: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: +micromatch@^4.0.2: version "4.0.4" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -6176,6 +6165,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +platform@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + please-upgrade-node@^3.0.2, please-upgrade-node@^3.1.1: version "3.2.0" resolved "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"