├── .github ├── dependabot.yml └── workflows │ ├── backport.yml │ ├── docs-test.yml │ ├── docs.yml │ ├── nodejs.yml │ └── stale.yml ├── .gitignore ├── .npmignore ├── .taprc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING ├── LICENSE ├── README.md ├── benchmark ├── fixtures │ └── add.js ├── piscina-queue-comparison.js ├── queue-comparison.js ├── simple-benchmark-async.js ├── simple-benchmark-fixed-queue.js └── simple-benchmark.js ├── docs ├── .gitignore ├── CNAME ├── README.md ├── babel.config.js ├── docs │ ├── Introduction.md │ ├── advanced-topics │ │ ├── _category_.json │ │ ├── custom-task-queues.mdx │ │ ├── loadbalancer.mdx │ │ └── performance.mdx │ ├── api-reference │ │ ├── _category_.json │ │ ├── api-overview.md │ │ ├── class.md │ │ ├── event.md │ │ ├── interface.md │ │ ├── method.md │ │ ├── property.md │ │ └── static-property.md │ ├── examples │ │ ├── _category_.json │ │ ├── abort.mdx │ │ ├── async_load.mdx │ │ ├── broadcast.mdx │ │ ├── electron.mdx │ │ ├── es-module.mdx │ │ ├── message-port.mdx │ │ ├── messages.mdx │ │ ├── move.mdx │ │ ├── multiple-workers-one-file.mdx │ │ ├── multiple-workers.mdx │ │ ├── n-api.mdx │ │ ├── named.mdx │ │ ├── progress.mdx │ │ ├── react-ssr.mdx │ │ ├── resource-limit.mdx │ │ ├── scrypt.mdx │ │ ├── server.mdx │ │ ├── simple.mdx │ │ ├── simple_async.mdx │ │ ├── stream-in.mdx │ │ ├── stream.mdx │ │ ├── task-queue.mdx │ │ ├── webstreams-transform.mdx │ │ ├── webstreams.mdx │ │ └── worker-options.mdx │ ├── getting-started │ │ ├── Installation.mdx │ │ ├── _category_.json │ │ ├── basic-usage.mdx │ │ ├── managing-worker-threads.mdx │ │ └── typescript.mdx │ └── update-log │ │ ├── _category_.json │ │ ├── changelog.md │ │ └── release-notes.md ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ └── WorkerWrapper.mdx │ ├── css │ │ └── custom.css │ └── pages │ │ └── index.module.css ├── static │ ├── .nojekyll │ └── img │ │ ├── banner.jpg │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg └── tsconfig.json ├── eslint.config.js ├── examples ├── abort │ ├── index.js │ ├── index2.js │ ├── index3.js │ ├── index4.js │ └── worker.js ├── async_load │ ├── index.js │ ├── worker.js │ └── worker.mjs ├── broadcast │ ├── main.js │ └── worker.js ├── es-module │ ├── index.mjs │ └── worker.mjs ├── message_port │ ├── index.js │ └── worker.js ├── messages │ ├── index.js │ └── worker.js ├── move │ ├── index.js │ └── worker.js ├── multiple-workers-one-file │ ├── index.js │ ├── index.mjs │ ├── worker.js │ └── worker.mjs ├── multiple-workers │ ├── add_worker.js │ ├── index.js │ └── multiply_worker.js ├── n-api │ ├── .gitignore │ ├── README.md │ ├── binding.gyp │ ├── example.cc │ ├── example │ │ ├── index.js │ │ └── package.json │ ├── index.js │ └── package.json ├── named │ ├── helper.js │ ├── index.js │ └── worker.js ├── progress │ ├── index.js │ ├── package.json │ └── worker.js ├── react-ssr │ ├── components │ │ ├── greeting.js │ │ ├── index.js │ │ ├── lorem.js │ │ └── package.json │ ├── package.json │ ├── pooled.js │ ├── unpooled.js │ └── worker.js ├── resourceLimits │ ├── index.js │ └── worker.js ├── scrypt │ ├── README.md │ ├── monitor.js │ ├── package.json │ ├── pooled.js │ ├── pooled_sync.js │ ├── scrypt.js │ ├── scrypt_sync.js │ ├── unpooled.js │ └── unpooled_sync.js ├── server │ ├── README.md │ ├── async-sleep-pooled.js │ ├── async-sleep-unpooled.js │ ├── package.json │ ├── sync-sleep-pooled.js │ ├── sync-sleep-unpooled.js │ ├── worker.js │ └── worker2.js ├── simple │ ├── index.js │ └── worker.js ├── simple_async │ ├── index.js │ └── worker.js ├── stream-in │ ├── .gitignore │ ├── generate.js │ ├── index.js │ ├── package.json │ ├── progress.js │ └── worker.js ├── stream │ ├── index.mjs │ ├── stream.mjs │ └── worker.mjs ├── task-queue │ ├── index.js │ ├── package.json │ └── worker.js ├── typescript │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── webstreams-transform │ ├── index.mjs │ └── worker.mjs ├── webstreams │ ├── index.mjs │ └── worker.mjs └── worker_options │ ├── index.js │ └── worker.js ├── package-lock.json ├── package.json ├── src ├── abort.ts ├── common.ts ├── errors.ts ├── histogram.ts ├── index.ts ├── main.ts ├── symbols.ts ├── task_queue │ ├── array_queue.ts │ ├── common.ts │ ├── fixed_queue.ts │ └── index.ts ├── types.ts ├── worker.ts └── worker_pool │ ├── balancer │ └── index.ts │ ├── base.ts │ └── index.ts ├── test ├── abort-task.ts ├── async-context.ts ├── atomics-optimization.ts ├── console-log.ts ├── fixed-queue.ts ├── fixtures │ ├── console-log.ts │ ├── esm-async.mjs │ ├── esm-export.mjs │ ├── eval-async.js │ ├── eval-params.js │ ├── eval.js │ ├── move.ts │ ├── multiple.js │ ├── notify-then-sleep-or.js │ ├── notify-then-sleep-or.ts │ ├── notify-then-sleep.ts │ ├── resource-limits.js │ ├── send-buffer-then-get-length.js │ ├── send-transferrable-then-get-length.js │ ├── simple-isworkerthread-named-import.ts │ ├── simple-isworkerthread.ts │ ├── simple-workerdata-named-import.ts │ ├── simple-workerdata.ts │ ├── sleep.js │ ├── vm.js │ ├── wait-for-notify.js │ ├── wait-for-notify.ts │ └── wait-for-others.ts ├── generics.ts ├── histogram.ts ├── idle-timeout.ts ├── issue-513.ts ├── load-with-esm.ts ├── messages.ts ├── move-test.ts ├── nice.ts ├── option-validation.ts ├── pool-close.ts ├── pool-destroy.ts ├── pool.ts ├── post-task.ts ├── simple-test.ts ├── task-queue.ts ├── test-is-buffer-transferred.ts ├── test-resourcelimits.ts ├── test-uncaught-exception-from-handler.ts ├── thread-count.ts ├── tsconfig.json └── workers.ts └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request_target: 4 | types: 5 | - closed 6 | - labeled 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | backport: 14 | runs-on: ubuntu-latest 15 | if: > 16 | github.event.pull_request.merged 17 | && ( 18 | github.event.action == 'closed' 19 | || ( 20 | github.event.action == 'labeled' 21 | && contains(github.event.label.name, 'backport') 22 | ) 23 | ) 24 | name: Backport 25 | steps: 26 | - name: Backport 27 | uses: tibdex/backport@v2 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/docs-test.yml: -------------------------------------------------------------------------------- 1 | name: 'Documentation Build Test' 2 | on: 3 | pull_request: 4 | paths: 5 | - docs/** 6 | 7 | jobs: 8 | build: 9 | permissions: 10 | contents: read 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | 18 | - name: Use Node.js LTS 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 'lts/*' 22 | cache: 'yarn' 23 | cache-dependency-path: docs/package.json 24 | 25 | - name: Install Dependencies 26 | run: | 27 | cd docs 28 | yarn install --frozen-lockfile 29 | 30 | - name: build 31 | run: | 32 | cd docs 33 | yarn build 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: 'Documentation' 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | permissions: 11 | contents: read 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | 19 | - name: Use Node.js LTS 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 'lts/*' 23 | cache: 'yarn' 24 | cache-dependency-path: docs/package.json 25 | 26 | - name: Install Dependencies 27 | run: | 28 | cd docs 29 | yarn install --frozen-lockfile 30 | 31 | - name: build 32 | run: | 33 | cd docs 34 | yarn build 35 | 36 | - name: Upload Build Artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: docs/build 40 | 41 | deploy: 42 | permissions: 43 | pages: write 44 | id-token: write 45 | name: Deploy to GH Pages 46 | needs: [build] 47 | runs-on: ubuntu-latest 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - current 5 | - next 6 | - 'v*' 7 | pull_request: 8 | paths-ignore: 9 | - docs/** 10 | 11 | name: CI 12 | 13 | jobs: 14 | lint: 15 | permissions: 16 | contents: read 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: v22.x 25 | cache: 'npm' 26 | cache-dependency-path: package.json 27 | 28 | - name: Install dependencies 29 | run: npm install 30 | - name: Check linting 31 | run: npm run lint 32 | 33 | tests: 34 | permissions: 35 | contents: read 36 | name: Tests 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | os: [ubuntu-latest, macos-latest, windows-latest] 41 | node-version: [20.x, 22.x, 23.x] 42 | runs-on: ${{matrix.os}} 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | persist-credentials: false 47 | 48 | - name: Use Node.js ${{ matrix.node-version }} 49 | uses: actions/setup-node@v4 50 | with: 51 | node-version: ${{ matrix.node-version }} 52 | cache: 'npm' 53 | cache-dependency-path: package.json 54 | 55 | - name: Install Dependencies 56 | run: npm install 57 | 58 | - name: Run Tests 59 | run: npm run test:ci 60 | 61 | automerge: 62 | if: > 63 | github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' 64 | needs: 65 | - tests 66 | runs-on: ubuntu-latest 67 | permissions: 68 | contents: write 69 | pull-requests: write 70 | steps: 71 | - name: Merge Dependabot PR 72 | uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 73 | with: 74 | github-token: ${{ secrets.GITHUB_TOKEN }} 75 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '0 10 * * 0' 5 | 6 | jobs: 7 | stale: 8 | permissions: 9 | pull-requests: write 10 | issues: write 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | stale-issue-message: 'This issue has been marked as stale because it has been opened 30 days without activity. Remove stale label or comment or this will be closed in 5 days.' 16 | stale-pr-message: 'This issue has been marked as stale because it has been opened 45 days without activity. Remove stale label or comment or this will be closed in 10 days.' 17 | stale-issue-label: 'stale' 18 | stale-pr-label: 'stale' 19 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 20 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 21 | days-before-issue-stale: 30 22 | days-before-pr-stale: 30 23 | days-before-issue-close: 5 24 | days-before-pr-close: 10 25 | exempt-issue-labels: never-stale,enhancement,v4.x,v5.x,good-first-issue,help-wanted,semver-major,semver-minor 26 | exempt-pr-labels: wip,never-stale,v4.x,v5.x,semver-major,semver-minor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .vscode 3 | node_modules 4 | dist 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .nyc_output 3 | package-lock.json 4 | coverage 5 | examples 6 | docs/**/* 7 | !docs/docs/**/*.md -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | # Coverage done by c8 2 | check-coverage: false 3 | coverage: false 4 | coverage-report: 5 | - lcovonly 6 | color: true 7 | jobs: 2 8 | test-env: TS_NODE_PROJECT=test/tsconfig.json 9 | test-ignore: $. 10 | test-regex: ((\/|^)(test?|__test?__)\/.*|\.(tests?|spec)|^\/?tests?)\.([mc]js|ts)$ 11 | timeout: 60 12 | ts: true 13 | jsx: false 14 | flow: false 15 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # Piscina is an OPEN Open Source Project 2 | 3 | ## What? 4 | 5 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 6 | 7 | ## Rules 8 | 9 | There are a few basic ground-rules for contributors: 10 | 11 | 1. **No `--force` pushes** on `master` or modifying the Git history in any way after a PR has been merged. 12 | 1. **Non-master branches** ought to be used for ongoing work. 13 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 14 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 15 | 1. Contributors should attempt to adhere to the prevailing code-style. 16 | 1. 100% code coverage 17 | 1. Semantic Versioning is used. 18 | 19 | ## Releases 20 | 21 | Declaring formal releases remains the prerogative of the project maintainer. 22 | 23 | ## Changes to this arrangement 24 | 25 | This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 26 | 27 | ----------------------------------------- 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 James M Snell and the Piscina contributors 4 | 5 | Piscina contributors listed at https://github.com/jasnell/piscina#the-team and 6 | in the README file. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /benchmark/fixtures/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = ({ a, b }) => a + b; 3 | -------------------------------------------------------------------------------- /benchmark/piscina-queue-comparison.js: -------------------------------------------------------------------------------- 1 | const { Bench } = require('tinybench'); 2 | const { Piscina, FixedQueue, ArrayTaskQueue } = require('..'); 3 | const { resolve } = require('node:path'); 4 | 5 | const QUEUE_SIZE = 100_000; 6 | 7 | const bench = new Bench({ time: 100, warmup: true }); 8 | 9 | bench 10 | .add('Piscina with ArrayTaskQueue', async () => { 11 | const queue = new ArrayTaskQueue(); 12 | const pool = new Piscina({ 13 | filename: resolve(__dirname, 'fixtures/add.js'), 14 | taskQueue: queue 15 | }); 16 | const tasks = []; 17 | for (let i = 0; i < QUEUE_SIZE; i++) { 18 | tasks.push(pool.run({ a: 4, b: 6 })); 19 | } 20 | await Promise.all(tasks); 21 | await pool.destroy(); 22 | }) 23 | .add('Piscina with FixedQueue', async () => { 24 | const queue = new FixedQueue(); 25 | const pool = new Piscina({ 26 | filename: resolve(__dirname, 'fixtures/add.js'), 27 | taskQueue: queue 28 | }); 29 | const tasks = []; 30 | for (let i = 0; i < QUEUE_SIZE; i++) { 31 | tasks.push(pool.run({ a: 4, b: 6 })); 32 | } 33 | await Promise.all(tasks); 34 | await pool.destroy(); 35 | }); 36 | 37 | (async () => { 38 | await bench.run(); 39 | 40 | console.table(bench.table()); 41 | })(); 42 | -------------------------------------------------------------------------------- /benchmark/queue-comparison.js: -------------------------------------------------------------------------------- 1 | const { Bench } = require('tinybench'); 2 | const { ArrayTaskQueue, FixedQueue } = require('..'); 3 | 4 | const QUEUE_SIZE = 100_000; 5 | 6 | const bench = new Bench({ time: 100, warmup: true }); 7 | 8 | bench 9 | .add('ArrayTaskQueue full push + full shift', async () => { 10 | const queue = new ArrayTaskQueue(); 11 | for (let i = 0; i < QUEUE_SIZE; i++) { 12 | queue.push(i); 13 | } 14 | for (let i = 0; i < QUEUE_SIZE; i++) { 15 | queue.shift(); 16 | } 17 | }) 18 | .add('FixedQueue full push + full shift', async () => { 19 | const queue = new FixedQueue(); 20 | for (let i = 0; i < QUEUE_SIZE; i++) { 21 | queue.push(i); 22 | } 23 | for (let i = 0; i < QUEUE_SIZE; i++) { 24 | queue.shift(); 25 | } 26 | }); 27 | 28 | (async () => { 29 | await bench.run(); 30 | 31 | console.table(bench.table()); 32 | })(); 33 | -------------------------------------------------------------------------------- /benchmark/simple-benchmark-async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Piscina } = require('../dist'); 3 | const { resolve } = require('path'); 4 | 5 | async function simpleBenchmark ({ duration = 10000 } = {}) { 6 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/add.js'), atomics: 'async' }); 7 | let done = 0; 8 | 9 | const results = []; 10 | const start = process.hrtime.bigint(); 11 | while (pool.queueSize === 0) { 12 | results.push(scheduleTasks()); 13 | } 14 | 15 | async function scheduleTasks () { 16 | while ((process.hrtime.bigint() - start) / 1_000_000n < duration) { 17 | await pool.run({ a: 4, b: 6 }); 18 | done++; 19 | } 20 | } 21 | 22 | await Promise.all(results); 23 | 24 | return done / duration * 1e3; 25 | } 26 | 27 | simpleBenchmark().then((opsPerSecond) => { 28 | console.log(`opsPerSecond: ${opsPerSecond} (with default taskQueue)`); 29 | }); 30 | -------------------------------------------------------------------------------- /benchmark/simple-benchmark-fixed-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Piscina, FixedQueue } = require('..'); 3 | 4 | const { resolve } = require('path'); 5 | 6 | async function simpleBenchmark ({ duration = 10000 } = {}) { 7 | const pool = new Piscina({ 8 | filename: resolve(__dirname, 'fixtures/add.js'), 9 | taskQueue: new FixedQueue() 10 | }); 11 | let done = 0; 12 | 13 | const results = []; 14 | const start = process.hrtime.bigint(); 15 | while (pool.queueSize === 0) { 16 | results.push(scheduleTasks()); 17 | } 18 | 19 | async function scheduleTasks () { 20 | while ((process.hrtime.bigint() - start) / 1_000_000n < duration) { 21 | await pool.run({ a: 4, b: 6 }); 22 | done++; 23 | } 24 | } 25 | 26 | await Promise.all(results); 27 | 28 | return done / duration * 1e3; 29 | } 30 | 31 | simpleBenchmark().then((opsPerSecond) => { 32 | console.log(`opsPerSecond: ${opsPerSecond} (with FixedQueue as taskQueue)`); 33 | }); 34 | -------------------------------------------------------------------------------- /benchmark/simple-benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Piscina } = require('..'); 3 | const { resolve } = require('path'); 4 | 5 | async function simpleBenchmark ({ duration = 10000 } = {}) { 6 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/add.js') }); 7 | let done = 0; 8 | 9 | const results = []; 10 | const start = process.hrtime.bigint(); 11 | while (pool.queueSize === 0) { 12 | results.push(scheduleTasks()); 13 | } 14 | 15 | async function scheduleTasks () { 16 | while ((process.hrtime.bigint() - start) / 1_000_000n < duration) { 17 | await pool.run({ a: 4, b: 6 }); 18 | done++; 19 | } 20 | } 21 | 22 | await Promise.all(results); 23 | 24 | return done / duration * 1e3; 25 | } 26 | 27 | simpleBenchmark().then((opsPerSecond) => { 28 | console.log(`opsPerSecond: ${opsPerSecond} (with default taskQueue)`); 29 | }); 30 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | piscinajs.dev 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')] 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/Introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: / 4 | --- 5 | 6 | # Introduction 7 | 8 | Piscina.js is a powerful Node.js worker pool library that allows you to efficiently run CPU-intensive tasks in parallel using worker threads. It provides a simple API for offloading computationally expensive tasks to a pool of worker threads, thereby improving the performance and scalability of your Node.js applications. 9 | 10 | ## Why Piscina? 11 | 12 | In the early days of worker threads, the Node.js core team encountered an issue where a user's application was spinning up thousands of concurrent worker threads, leading to performance issues. While this specific issue helped identify a minor memory leak in the worker implementation, it highlighted a broader problem: the misuse of worker threads due to a lack of understanding. 13 | 14 | While worker threads have matured and their usage has become more widespread, there is still a need for better examples and education around their correct usage. This realization led to the creation of Piscina, an open-source project sponsored by [NearForm Research](https://www.nearform.com/), focused on providing guidance and best practices for using worker threads in Node.js applications. 15 | 16 | With worker threads now a well-established feature in Node.js, Piscina aims to bridge the gap between the potential of worker threads and their practical implementation. 17 | 18 | ## Key features 19 | 20 | ✔ Fast communication between threads\ 21 | ✔ Covers both fixed-task and variable-task scenarios\ 22 | ✔ Supports flexible pool sizes\ 23 | ✔ Proper async tracking integration\ 24 | ✔ Tracking statistics for run and wait times\ 25 | ✔ Cancellation Support\ 26 | ✔ Supports enforcing memory resource limits\ 27 | ✔ Supports CommonJS, ESM, and TypeScript\ 28 | ✔ Custom task queues\ 29 | ✔ Optional CPU scheduling priorities on Linux 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/docs/advanced-topics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Advanced topics", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "In this section, you'll find information on more complex features of Piscina like setting up your own task queues and tips for making your application run faster. This part of the documentation is great for when you want to get more out of Piscina and understand how to manage tasks and performance better." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/advanced-topics/loadbalancer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Custom Load Balancers 3 | sidebar_position: 2 4 | --- 5 | 6 | [Load Balancers](https://www.cloudflare.com/learning/performance/what-is-load-balancing/) are a crucial part of any distributed workload. 7 | Piscina by default uses a [Resource Based](https://www.cloudflare.com/en-gb/learning/performance/types-of-load-balancing-algorithms/) algorithm (least-busy) 8 | to distribute the load across the different workers set by the pool. 9 | 10 | ### Choosing the Load Balancing Algorithm 11 | 12 | Choosing the right algorithm heavily depends on the requirements of each individual problem, and to assess 13 | them by testing the implementation against several variations of workloads. 14 | 15 | The algorithms can be grouped on: 16 | 17 | #### Dynamic 18 | 19 | Focused on taking decision based on heuristics from the workload. 20 | 21 | It aims to balance the work by adapting itself to the environment and distribute the workloads equally across the different workers/nodes attempting to make better use 22 | of resources or to minimize the time to finish each individual workload. 23 | 24 | > Piscina uses a Dynamic algorithm, based on how busy the worker is (least-busy) 25 | 26 | #### Static 27 | 28 | Aims to distribute the workload based on a group of predefined factors. 29 | It does not adapt to the environment, but rather aims to preserve the state of the distribution accordingly 30 | to the values set beforehand. 31 | 32 | This can be helpful under several situations where we want the workload to be distributed evenly or stick to a specific worker/node over time. 33 | 34 | > Round Robin, Ring Hash, are just examples of these algorithms 35 | 36 | It is heavily advised to understand the problem and the workload your pool will be facing, and tests heavily against the different algorithms that matches your 37 | use case with several combinations to better understand its impact and how to manage it. 38 | 39 | ### Resources 40 | 41 | - [Load Balancing Algorithms](https://en.wikipedia.org/wiki/Load_balancing_(computing)) 42 | - [Types of Load Balancing Algorithms](https://www.cloudflare.com/en-gb/learning/performance/types-of-load-balancing-algorithms/) -------------------------------------------------------------------------------- /docs/docs/api-reference/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "API Reference", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "This section provides a detailed explanation of the classes, methods, properties, events, and other components that make up the library" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/api-reference/event.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Events 3 | sidebar_position: 4 4 | --- 5 | 6 | ## Event: `'error'` 7 | 8 | An `'error'` event is emitted by instances of this class when: 9 | 10 | - Uncaught exceptions occur inside Worker threads that do not currently handle 11 | tasks. 12 | - Unexpected messages are sent from from Worker threads. 13 | 14 | All other errors are reported by rejecting the `Promise` returned from 15 | `run()` or `runTask()`, including rejections reported by the handler function 16 | itself. 17 | 18 | ## Event: `'drain'` 19 | 20 | A `'drain'` event is emitted whenever the `queueSize` reaches `0`. 21 | 22 | ## Event: `'needsDrain'` 23 | 24 | Similar to [`Piscina#needsDrain`](https://github.com/piscinajs/piscina#property-needsdrain-readonly); 25 | this event is triggered once the total capacity of the pool is exceeded 26 | by number of tasks enqueued that are pending of execution. 27 | 28 | ## Event: `'message'` 29 | 30 | A `'message'` event is emitted whenever a message is received from a worker thread. 31 | 32 | ## Event: `'workerCreate'` 33 | 34 | Event that is triggered when a new worker is created. 35 | 36 | As argument, it receives the worker instance. 37 | 38 | ## Event: `'workerDestroy'` 39 | 40 | Event that is triggered when a worker is destroyed. 41 | 42 | As argument, it receives the worker instance that has been destroyed. -------------------------------------------------------------------------------- /docs/docs/api-reference/interface.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Interface 3 | sidebar_position: 7 4 | --- 5 | 6 | ## Interface: `Transferable` 7 | 8 | Objects may implement the `Transferable` interface to create their own 9 | custom transferable objects. This is useful when an object being 10 | passed into or from a worker contains a deeply nested transferable 11 | object such as an `ArrayBuffer` or `MessagePort`. 12 | 13 | `Transferable` objects expose two properties inspected by Piscina 14 | to determine how to transfer the object. These properties are 15 | named using the special static `Piscina.transferableSymbol` and 16 | `Piscina.valueSymbol` properties: 17 | 18 | * The `Piscina.transferableSymbol` property provides the object 19 | (or objects) that are to be included in the `transferList`. 20 | 21 | * The `Piscina.valueSymbol` property provides a surrogate value 22 | to transmit in place of the `Transferable` itself. 23 | 24 | Both properties are required. 25 | 26 | For example: 27 | 28 | ```js 29 | const { 30 | move, 31 | transferableSymbol, 32 | valueSymbol 33 | } = require('piscina'); 34 | 35 | module.exports = () => { 36 | const obj = { 37 | a: { b: new Uint8Array(5); }, 38 | c: { new Uint8Array(10); }, 39 | 40 | get [transferableSymbol]() { 41 | // Transfer the two underlying ArrayBuffers 42 | return [this.a.b.buffer, this.c.buffer]; 43 | } 44 | 45 | get [valueSymbol]() { 46 | return { a: { b: this.a.b }, c: this.c }; 47 | } 48 | }; 49 | return move(obj); 50 | }; 51 | ``` -------------------------------------------------------------------------------- /docs/docs/api-reference/method.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Methods 3 | sidebar_position: 3 4 | --- 5 | ## Method: `run(task[, options])` 6 | 7 | Schedules a task to be run on a Worker thread. 8 | 9 | * `task`: Any value. This will be passed to the function that is exported from 10 | `filename`. 11 | * `options`: 12 | * `transferList`: An optional lists of objects that is passed to 13 | [`postMessage()`] when posting `task` to the Worker, which are transferred 14 | rather than cloned. 15 | * `filename`: Optionally overrides the `filename` option passed to the 16 | constructor for this task. If no `filename` was specified to the constructor, 17 | this is mandatory. 18 | * `name`: Optionally overrides the exported worker function used for the task. 19 | * `abortSignal`: An `AbortSignal` instance. If passed, this can be used to 20 | cancel a task. If the task is already running, the corresponding `Worker` 21 | thread will be stopped. 22 | (More generally, any `EventEmitter` or `EventTarget` that emits `'abort'` 23 | events can be passed here.) Abortable tasks cannot share threads regardless 24 | of the `concurrentTasksPerWorker` options. 25 | 26 | This returns a `Promise` for the return value of the (async) function call 27 | made to the function exported from `filename`. If the (async) function throws 28 | an error, the returned `Promise` will be rejected with that error. 29 | If the task is aborted, the returned `Promise` is rejected with an error 30 | as well. 31 | 32 | ## Method: `runTask(task[, transferList][, filename][, abortSignal])` 33 | 34 | **Deprecated** -- Use `run(task, options)` instead. 35 | 36 | Schedules a task to be run on a Worker thread. 37 | 38 | * `task`: Any value. This will be passed to the function that is exported from 39 | `filename`. 40 | * `transferList`: An optional lists of objects that is passed to 41 | [`postMessage()`] when posting `task` to the Worker, which are transferred 42 | rather than cloned. 43 | * `filename`: Optionally overrides the `filename` option passed to the 44 | constructor for this task. If no `filename` was specified to the constructor, 45 | this is mandatory. 46 | * `signal`: An [`AbortSignal`][] instance. If passed, this can be used to 47 | cancel a task. If the task is already running, the corresponding `Worker` 48 | thread will be stopped. 49 | (More generally, any `EventEmitter` or `EventTarget` that emits `'abort'` 50 | events can be passed here.) Abortable tasks cannot share threads regardless 51 | of the `concurrentTasksPerWorker` options. 52 | 53 | This returns a `Promise` for the return value of the (async) function call 54 | made to the function exported from `filename`. If the (async) function throws 55 | an error, the returned `Promise` will be rejected with that error. 56 | If the task is aborted, the returned `Promise` is rejected with an error 57 | as well. 58 | 59 | ## Method: `destroy()` 60 | 61 | Stops all Workers and rejects all `Promise`s for pending tasks. 62 | 63 | This returns a `Promise` that is fulfilled once all threads have stopped. 64 | 65 | ## Method: `close([options])` 66 | 67 | * `options`: 68 | * `force`: A `boolean` value that indicates whether to abort all tasks that 69 | are enqueued but not started yet. The default is `false`. 70 | 71 | It stops all Workers gracefully. 72 | 73 | This returns a `Promise` that is fulfilled once all tasks that were started 74 | have completed and all threads have stopped. 75 | 76 | This method is similar to `destroy()`, but with the difference that `close()` 77 | will wait for the worker tasks to finish, while `destroy()` 78 | will abort them immediately. -------------------------------------------------------------------------------- /docs/docs/api-reference/property.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Properties 3 | sidebar_position: 5 4 | --- 5 | 6 | ## Property: `completed` (readonly) 7 | 8 | The current number of completed tasks. 9 | 10 | ## Property: `duration` (readonly) 11 | 12 | The length of time (in milliseconds) since this `Piscina` instance was 13 | created. 14 | 15 | ## Property: `options` (readonly) 16 | 17 | A copy of the options that are currently being used by this instance. This 18 | object has the same properties as the options object passed to the constructor. 19 | 20 | ## Property: `runTime` (readonly) 21 | 22 | A histogram summary object summarizing the collected run times of completed 23 | tasks. All values are expressed in milliseconds. 24 | 25 | * `runTime.average` {`number`} The average run time of all tasks 26 | * `runTime.mean` {`number`} The mean run time of all tasks 27 | * `runTime.stddev` {`number`} The standard deviation of collected run times 28 | * `runTime.min` {`number`} The fastest recorded run time 29 | * `runTime.max` {`number`} The slowest recorded run time 30 | 31 | All properties following the pattern `p{N}` where N is a number (e.g. `p1`, `p99`) 32 | represent the percentile distributions of run time observations. For example, 33 | `p99` is the 99th percentile indicating that 99% of the observed run times were 34 | faster or equal to the given value. 35 | 36 | ```js 37 | { 38 | average: 1880.25, 39 | mean: 1880.25, 40 | stddev: 1.93, 41 | min: 1877, 42 | max: 1882.0190887451172, 43 | p0_001: 1877, 44 | p0_01: 1877, 45 | p0_1: 1877, 46 | p1: 1877, 47 | p2_5: 1877, 48 | p10: 1877, 49 | p25: 1877, 50 | p50: 1881, 51 | p75: 1881, 52 | p90: 1882, 53 | p97_5: 1882, 54 | p99: 1882, 55 | p99_9: 1882, 56 | p99_99: 1882, 57 | p99_999: 1882 58 | } 59 | ``` 60 | 61 | ## Property: `threads` (readonly) 62 | 63 | An Array of the `Worker` instances used by this pool. 64 | 65 | ## Property: `queueSize` (readonly) 66 | 67 | The current number of tasks waiting to be assigned to a Worker thread. 68 | 69 | ## Property: `needsDrain` (readonly) 70 | 71 | Boolean value that specifies whether the capacity of the pool has 72 | been exceeded by the number of tasks submitted. 73 | 74 | This property is helpful to make decisions towards creating backpressure 75 | over the number of tasks submitted to the pool. 76 | 77 | ## Property: `utilization` (readonly) 78 | 79 | A point-in-time ratio comparing the approximate total mean run time 80 | of completed tasks to the total runtime capacity of the pool. 81 | 82 | A pools runtime capacity is determined by multiplying the `duration` 83 | by the `options.maxThread` count. This provides an absolute theoretical 84 | maximum aggregate compute time that the pool would be capable of. 85 | 86 | The approximate total mean run time is determined by multiplying the 87 | mean run time of all completed tasks by the total number of completed 88 | tasks. This number represents the approximate amount of time the 89 | pool as been actively processing tasks. 90 | 91 | The utilization is then calculated by dividing the approximate total 92 | mean run time by the capacity, yielding a fraction between `0` and `1`. 93 | 94 | ## Property: `waitTime` (readonly) 95 | 96 | A histogram summary object summarizing the collected times tasks spent 97 | waiting in the queue. All values are expressed in milliseconds. 98 | 99 | * `waitTime.average` {`number`} The average wait time of all tasks 100 | * `waitTime.mean` {`number`} The mean wait time of all tasks 101 | * `waitTime.stddev` {`number`} The standard deviation of collected wait times 102 | * `waitTime.min` {`number`} The fastest recorded wait time 103 | * `waitTime.max` {`number`} The longest recorded wait time 104 | 105 | All properties following the pattern `p{N}` where N is a number (e.g. `p1`, `p99`) 106 | represent the percentile distributions of wait time observations. For example, 107 | `p99` is the 99th percentile indicating that 99% of the observed wait times were 108 | faster or equal to the given value. 109 | 110 | ```js 111 | { 112 | average: 1880.25, 113 | mean: 1880.25, 114 | stddev: 1.93, 115 | min: 1877, 116 | max: 1882.0190887451172, 117 | p0_001: 1877, 118 | p0_01: 1877, 119 | p0_1: 1877, 120 | p1: 1877, 121 | p2_5: 1877, 122 | p10: 1877, 123 | p25: 1877, 124 | p50: 1881, 125 | p75: 1881, 126 | p90: 1882, 127 | p97_5: 1882, 128 | p99: 1882, 129 | p99_9: 1882, 130 | p99_99: 1882, 131 | p99_999: 1882 132 | } 133 | ``` -------------------------------------------------------------------------------- /docs/docs/api-reference/static-property.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Static Properties and Methods 3 | sidebar_position: 6 4 | --- 5 | 6 | ## Static property: `isWorkerThread` (readonly) 7 | 8 | Is `true` if this code runs inside a `Piscina` threadpool as a Worker. 9 | 10 | ## Static property: `version` (readonly) 11 | 12 | Provides the current version of this library as a semver string. 13 | 14 | ## Static method: `move(value)` 15 | 16 | By default, any value returned by a worker function will be cloned when 17 | returned back to the Piscina pool, even if that object is capable of 18 | being transfered. The `Piscina.move()` method can be used to wrap and 19 | mark transferable values such that they will by transfered rather than 20 | cloned. 21 | 22 | The `value` may be any object supported by Node.js to be transferable 23 | (e.g. `ArrayBuffer`, any `TypedArray`, or `MessagePort`), or any object 24 | implementing the `Transferable` interface. 25 | 26 | ```js 27 | const { move } = require('piscina'); 28 | 29 | module.exports = () => { 30 | return move(new ArrayBuffer(10)); 31 | } 32 | ``` 33 | 34 | The `move()` method will throw if the `value` is not transferable. 35 | 36 | The object returned by the `move()` method should not be set as a 37 | nested value in an object. If it is used, the `move()` object itself 38 | will be cloned as opposed to transfering the object it wraps. -------------------------------------------------------------------------------- /docs/docs/examples/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Examples", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Here are more ways to integrate Piscina into your projects. " 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/examples/async_load.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Async Load 3 | sidebar_position: 2 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Piscina supports asynchronously loaded workers. This feature allows you to perform asynchronous operations during worker initialization, such as loading configurations or establishing database connections. 11 | 12 | For example, the main script below creates a Piscina pool and runs tasks from two worker files. 13 | 14 | 15 | 16 | ```js title="main.js" 17 | "use strict"; 18 | 19 | const Piscina = require("../.."); 20 | 21 | const pool = new Piscina(); 22 | const { resolve } = require("path"); 23 | 24 | (async () => { 25 | await Promise.all([ 26 | pool.run({}, { filename: resolve(__dirname, "worker") }), 27 | pool.run({}, { filename: resolve(__dirname, "worker.mjs") }), 28 | ]); 29 | })(); 30 | 31 | ```` 32 | 33 | 34 | 35 | ```javascript title="main.ts" 36 | import Piscina from "piscina"; 37 | import { resolve } from "path"; 38 | import { filename } from "./worker"; 39 | 40 | const pool = new Piscina({ 41 | workerData: { fullpath: filename }, 42 | }); 43 | 44 | (async () => { 45 | try { 46 | await Promise.all([ 47 | pool.run( 48 | {}, 49 | { 50 | name: "callSleep", 51 | filename: resolve(__dirname, "./workerWrapper.js"), 52 | } 53 | ), 54 | pool.run({}, { filename: resolve(__dirname, "worker.mjs") }), 55 | ]); 56 | } catch (error) { 57 | console.log(error); 58 | } 59 | })(); 60 | 61 | ``` 62 | 63 | 64 | 65 | 66 | 67 | Both worker files demonstrate asynchronous loading. They use a sleep function to simulate an asynchronous operation that takes 500 milliseconds. 68 | 69 | 70 | 71 | ```js title="worker.js" 72 | "use strict"; 73 | 74 | const { promisify } = require("util"); 75 | const sleep = promisify(setTimeout); 76 | 77 | module.exports = (async () => { 78 | await sleep(500); 79 | return () => console.log("hello from an async loaded CommonJS worker"); 80 | })(); 81 | ``` 82 | 83 | 84 | 85 | ```js title="worker.ts" 86 | import { promisify } from "util"; 87 | import { resolve } from "path"; 88 | 89 | export const filename = resolve(__filename); 90 | 91 | const sleep = promisify(setTimeout); 92 | 93 | export async function callSleep(): Promise { 94 | await sleep(500); 95 | return console.log("hello from an async loaded TypeScript worker"); 96 | } 97 | ``` 98 | 99 | 100 | 101 | 102 | In `worker.mjs`: 103 | 104 | ```js title="worker.mjs" 105 | // eslint-disable-next-line no-eval 106 | import util from "util"; 107 | const sleep = util.promisify(setTimeout); 108 | 109 | async function load() { 110 | await sleep(500); 111 | return () => console.log("hello from an async loaded ESM worker"); 112 | } 113 | 114 | export default load(); 115 | ``` 116 | 117 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/async_load). -------------------------------------------------------------------------------- /docs/docs/examples/broadcast.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Broadcast 3 | sidebar_position: 3 4 | --- 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | import { WorkerWrapperComponent } from '@site/src/components/WorkerWrapper.mdx' 8 | 9 | In this example, we create a Piscina instance that uses BroadcastChannel(Node v18+) to implement broadcast communication. The main thread sends a message, and other threads the receive message. 10 | 11 | 12 | 13 | 14 | ```javascript title="index.js" 15 | 'use strict'; 16 | 17 | const { BroadcastChannel } = require('worker_threads'); 18 | const { resolve } = require('path'); 19 | 20 | const Piscina = require('piscina'); 21 | const piscina = new Piscina({ 22 | filename: resolve(__dirname, 'worker.js'), 23 | atomics: 'disabled' 24 | }); 25 | 26 | async function main () { 27 | const bc = new BroadcastChannel('my_channel'); 28 | // start worker 29 | Promise.all([ 30 | piscina.run('thread 1'), 31 | piscina.run('thread 2') 32 | ]); 33 | // post message in one second 34 | setTimeout(() => { 35 | bc.postMessage('Main thread message'); 36 | }, 1000); 37 | } 38 | 39 | main(); 40 | 41 | ``` 42 | 43 | ```javascript title="worker.js" 44 | 'use strict'; 45 | const { BroadcastChannel } = require('worker_threads'); 46 | 47 | module.exports = async (thread) => { 48 | const bc = new BroadcastChannel('my_channel'); 49 | bc.onmessage = (event) => { 50 | console.log(thread + ' Received from:' + event.data); 51 | }; 52 | await new Promise((resolve) => { 53 | setTimeout(resolve, 2000); 54 | }); 55 | }; 56 | 57 | ``` 58 | 59 | 60 | 61 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/broadcast). 62 | -------------------------------------------------------------------------------- /docs/docs/examples/electron.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Electron 3 | sidebar_position: 4 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Using workers and pooling within your application can greatly enhance performance and stability. The renderer is processed on the main thread, so any processor intensive tasks are going to cause hangups for users, either delivering a poor UX or in many cases, causing the dreaded `pthread_kill` and crashing the app. 11 | 12 | Implementing workers is relatively straight-forward in development, but you might run into some issues when packaging your build. 13 | 14 | ## electron-vite 15 | NB: While Electron / electron-vite officially support ESM, with the way that rollup packages the worker, electron ends up not being able read the worker. You must be using CJS in electron to read the worker modules. Besides, the bytecode plugin for obfuscating your code only works with CJS too. 16 | 17 | In your main file, import the path to your worker (not the wrapper - no wrapper is needed because of rollup) by appending `\*?modulePath` to the end of the path. 18 | 19 | ```typescript 20 | import workerPath from './worker?modulePath'; 21 | ``` 22 | 23 | During compilation, rollup will now automatically create a separate file for the worker in your main folder output at the given path. The path above resolves, and we can now use this in Piscina at run time. 24 | 25 | Create a pool with Piscina, and use the imported path instead. 26 | 27 | For those using TypeScript, the filename export referenced in the guide is not needed. 28 | 29 | ```typescript title="index.ts" 30 | const pool = new Piscina({ 31 | filename: workerPath, 32 | workerData: { 33 | userPath: path.join(app.getPath("userData")), 34 | }, 35 | }); 36 | ``` 37 | 38 | Electron-specific modules are NOT accessible inside workers. 39 | 40 | If you need access to information that electron provides, (e.g. `app.getPath("userData")`), you'll need to pass in that data as workerData. 41 | 42 | That's it! You can now use pool like normal in the Piscina docs. 43 | 44 | For further help, reference the worker section of the docs from electron-vite. 45 | 46 | https://electron-vite.org/guide/dev#worker-threads 47 | -------------------------------------------------------------------------------- /docs/docs/examples/es-module.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: ES Module 3 | sidebar_position: 5 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Piscina supports ES modules (ESM) out of the box. This example demonstrates how to use Piscina with ES modules in both the main script and the worker file. 11 | 12 | 13 | 14 | 15 | ```javascript title="index.mjs" 16 | import { Piscina } from "piscina"; 17 | 18 | const piscina = new Piscina({ 19 | filename: new URL("./worker.mjs", import.meta.url).href, 20 | }); 21 | 22 | (async () => { 23 | const result = await piscina.run({ a: 4, b: 6 }); 24 | console.log(result); 25 | })(); // Prints 10 26 | ``` 27 | 28 | 29 | 30 | 31 | 32 | ```typescript title="index.ts" 33 | import Piscina from "piscina"; 34 | import { resolve } from "path"; 35 | import { filename } from "./worker"; 36 | 37 | const piscina = new Piscina({ 38 | filename: resolve(__dirname, "./workerWrapper.js"), 39 | workerData: { fullpath: filename }, 40 | }); 41 | (async () => { 42 | const result = await piscina.run({ a: 4, b: 6 }); 43 | console.log(result); 44 | })(); 45 | ``` 46 | 47 | 48 | 49 | 50 | The worker file is a simple ES module that exports a default function: 51 | 52 | 53 | 54 | 55 | ```javascript title="worker.mjs" 56 | export default ({ a, b }) => { 57 | return a + b; 58 | }; 59 | ``` 60 | 61 | 62 | 63 | 64 | 65 | ```typescript title="worker.ts" 66 | interface Input { 67 | a: number; 68 | b: number; 69 | } 70 | 71 | export default ({ a, b }: Input): number => { 72 | return a + b; 73 | }; 74 | ``` 75 | 76 | 77 | 78 | 79 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/es-module). -------------------------------------------------------------------------------- /docs/docs/examples/message-port.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Message Port 3 | sidebar_position: 6 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Worker threads can receive `MessagePort` objects, enabling direct communication channels with the main thread. This feature is useful for scenarios that require continuous communication or transfer of large data sets between threads. 11 | 12 | The example below shows how to use `MessagePort` with Piscina. 13 | 14 | 15 | 16 | 17 | ```javascript title="index.js" 18 | 'use strict'; 19 | 20 | const Piscina = require('piscina'); 21 | const { resolve } = require('path'); 22 | const { MessageChannel } = require('worker_threads'); 23 | 24 | const piscina = new Piscina({ 25 | filename: resolve(__dirname, 'worker.js') 26 | }); 27 | 28 | (async function () { 29 | const channel = new MessageChannel(); 30 | channel.port2.on('message', (message) => { 31 | console.log(message); 32 | channel.port2.close(); 33 | }); 34 | await piscina.run({ port: channel.port1 }, { transferList: [channel.port1] }); 35 | })(); 36 | ``` 37 | 38 | 39 | 40 | 41 | 42 | ```typescript title="index.ts" 43 | import Piscina from "piscina"; 44 | import { resolve } from "path"; 45 | import { filename } from "./worker"; 46 | import { TransferListItem } from "piscina/dist/types"; 47 | 48 | const piscina = new Piscina({ 49 | filename: resolve(__dirname, "./workerWrapper.js"), 50 | workerData: { fullpath: filename }, 51 | }); 52 | 53 | interface CustomMessagePort extends MessagePort { 54 | on(event: string, listener: (message: any) => void): void; 55 | } 56 | (async function () { 57 | const channel = new MessageChannel(); 58 | (channel.port2 as CustomMessagePort).on('message', (message: string) => { 59 | console.log(message); 60 | channel.port2.close(); 61 | }); 62 | await piscina.run({ port: channel.port1 }, { transferList:[channel.port1] as TransferListItem}); 63 | })(); 64 | ``` 65 | 66 | 67 | 68 | 69 | The worker file receives the `MessagePort` and uses it to send a message back to the main thread: 70 | 71 | 72 | 73 | 74 | ```javascript title="worker.js" 75 | 'use strict'; 76 | 77 | module.exports = ({ port }) => { 78 | port.postMessage('hello from the worker pool'); 79 | }; 80 | ``` 81 | 82 | 83 | 84 | 85 | 86 | ```typescript title="worker.ts" 87 | import { MessagePort } from 'worker_threads'; 88 | 89 | interface WorkerInput { 90 | port: MessagePort; 91 | } 92 | 93 | export default ({ port }: WorkerInput): void => { 94 | port.postMessage('hello from the worker pool'); 95 | }; 96 | ``` 97 | 98 | 99 | 100 | 101 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/message_port). 102 | -------------------------------------------------------------------------------- /docs/docs/examples/messages.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Messages 3 | sidebar_position: 7 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Piscina allows workers to send messages back to the main thread using the `parentPort.postMessage()` method. This can be useful for sending progress updates, or intermediate results during the execution of a long-running task. 11 | 12 | 13 | 14 | 15 | ```javascript title="index.js" 16 | 'use strict'; 17 | 18 | const Piscina = require('piscina'); 19 | const { resolve } = require('path'); 20 | 21 | const piscina = new Piscina({ 22 | filename: resolve(__dirname, 'worker.js') 23 | }); 24 | 25 | (async function () { 26 | piscina.on('message', (event) => { 27 | console.log('Message received from worker: ', event); 28 | }); 29 | 30 | await piscina.run(); 31 | })(); 32 | ``` 33 | 34 | 35 | 36 | 37 | 38 | ```typescript title="index.ts" 39 | import Piscina from "piscina"; 40 | import { resolve } from "path"; 41 | import { filename } from "./worker"; 42 | import { TransferListItem } from "piscina/dist/types"; 43 | 44 | const piscina = new Piscina({ 45 | filename: resolve(__dirname, "./workerWrapper.js"), 46 | workerData: { fullpath: filename }, 47 | }); 48 | 49 | (async function () { 50 | piscina.on('message', (event: string) => { 51 | console.log('Message received from worker: ', event); 52 | }); 53 | 54 | await piscina.run({}); 55 | })(); 56 | ``` 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ```javascript title="worker.js" 65 | 'use strict'; 66 | const { parentPort } = require('worker_threads'); 67 | 68 | module.exports = () => { 69 | parentPort.postMessage('hello from the worker pool'); 70 | }; 71 | ``` 72 | 73 | 74 | 75 | 76 | 77 | ```typescript title="worker.ts" 78 | import { parentPort } from 'worker_threads'; 79 | 80 | export default (): void => { 81 | parentPort?.postMessage('hello from the worker pool'); 82 | }; 83 | ``` 84 | 85 | 86 | 87 | 88 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/messages). -------------------------------------------------------------------------------- /docs/docs/examples/move.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Move 3 | sidebar_position: 8 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Piscina provides a `move()` function that allows the transfer of data between the main thread and worker threads. The example below will show you how to use `Piscina.move()` to transfer `ArrayBuffer` without cloning, which can significantly improve performance for large data transfers. 11 | 12 | 13 | 14 | 15 | ```javascript title="index.js" 16 | const Piscina = require('piscina'); 17 | const { resolve } = require('path'); 18 | 19 | const pool = new Piscina({ 20 | filename: resolve(__dirname, 'worker.js'), 21 | idleTimeout: 1000 22 | }); 23 | 24 | (async () => { 25 | // The task will transfer an ArrayBuffer 26 | // back to the main thread rather than 27 | // cloning it. 28 | const u8 = await pool.run(Piscina.move(new Uint8Array(2))); 29 | console.log(u8.length); 30 | })(); 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | ```typescript title="index.ts" 38 | import Piscina from 'piscina'; 39 | import { resolve } from 'path'; 40 | 41 | const pool = new Piscina({ 42 | filename: resolve(__dirname, 'worker.ts'), 43 | idleTimeout: 1000 44 | }); 45 | 46 | (async () => { 47 | // The task will transfer an ArrayBuffer 48 | // back to the main thread rather than 49 | // cloning it. 50 | const u8 = await pool.run(Piscina.move(new Uint8Array(2))); 51 | console.log(u8.length); 52 | })(); 53 | ``` 54 | 55 | 56 | 57 | 58 | The worker file uses `move()` to transfer data back to the main thread: 59 | 60 | 61 | 62 | 63 | ```javascript title="worker.js" 64 | const { move } = require('piscina'); 65 | 66 | module.exports = () => { 67 | // Using move causes the Uint8Array to be 68 | // transferred rather than cloned. 69 | return move(new Uint8Array(10)); 70 | }; 71 | ``` 72 | 73 | 74 | 75 | 76 | 77 | ```typescript title="worker.ts" 78 | import { resolve } from "path"; 79 | import { move } from "piscina"; 80 | 81 | 82 | export const filename = resolve(__filename); 83 | 84 | export default (): Uint8Array => { 85 | // Using move causes the Uint8Array to be 86 | // transferred rather than cloned. 87 | return move(new Uint8Array(10)) as Uint8Array; 88 | }; 89 | 90 | ``` 91 | 92 | 93 | 94 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/move). -------------------------------------------------------------------------------- /docs/docs/examples/multiple-workers-one-file.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Multiple Workers in One File 3 | sidebar_position: 9 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Piscina allows you to define multiple worker functions within a single file. This approach can be useful when you have related tasks that you want to keep in one module. 11 | 12 | In the example below, for each task, `pool.run()` is called with two arguments: 13 | - The task data `({ a: 2, b: 3 })` 14 | - An options object specifying the name of the function to use. 15 | 16 | 17 | 18 | 19 | ```javascript title="index.js" 20 | 'use strict'; 21 | 22 | const Piscina = require('piscina'); 23 | const { resolve } = require('path'); 24 | 25 | const pool = new Piscina({ filename: resolve(__dirname, 'worker.js') }); 26 | 27 | (async () => { 28 | console.log(await Promise.all([ 29 | pool.run({ a: 2, b: 3 }, { name: 'add' }), 30 | pool.run({ a: 2, b: 3 }, { name: 'multiply' }) 31 | ])); 32 | })(); 33 | ``` 34 | 35 | ```javascript title="index.mjs" 36 | import { Piscina } from 'piscina'; 37 | 38 | const pool = new Piscina({ 39 | filename: new URL('./worker.mjs', import.meta.url).href 40 | }); 41 | 42 | console.log(await Promise.all([ 43 | pool.run({ a: 2, b: 3 }, { name: 'add' }), 44 | pool.run({ a: 2, b: 3 }, { name: 'multiply' }) 45 | ])); 46 | 47 | ``` 48 | 49 | 50 | 51 | 52 | 53 | ```typescript title="index.ts" 54 | import Piscina from 'piscina'; 55 | import { resolve } from 'path'; 56 | 57 | interface TaskInput { 58 | a: number; 59 | b: number; 60 | } 61 | 62 | const pool = new Piscina({ filename: resolve(__dirname, 'worker.ts') }); 63 | 64 | (async () => { 65 | console.log(await Promise.all([ 66 | pool.run({ a: 2, b: 3 }, { name: 'add' }), 67 | pool.run({ a: 2, b: 3 }, { name: 'multiply' }) 68 | ])); 69 | })(); 70 | ``` 71 | 72 | 73 | 74 | 75 | Here's the worker file that defines multiple functions: 76 | 77 | 78 | 79 | 80 | ```javascript title="worker.js" 81 | 'use strict'; 82 | 83 | function add ({ a, b }) { return a + b; } 84 | function multiply ({ a, b }) { return a * b; }; 85 | 86 | add.add = add; 87 | add.multiply = multiply; 88 | 89 | // The add function is the default handler, but 90 | // additional named handlers are exported. 91 | module.exports = add; 92 | ``` 93 | 94 | ```javascript title="worker.mjs" 95 | export function add ({ a, b }) { return a + b; } 96 | export function multiply ({ a, b }) { return a * b; }; 97 | ``` 98 | 99 | 100 | 101 | 102 | 103 | ```typescript title="worker.ts" 104 | import { resolve } from "path"; 105 | 106 | interface TaskInput { 107 | a: number; 108 | b: number; 109 | } 110 | 111 | function add({ a, b }: TaskInput): number { return a + b; } 112 | function multiply({ a, b }: TaskInput): number { return a * b; } 113 | 114 | export { add, multiply }; 115 | export const filename = resolve(__filename); 116 | ``` 117 | 118 | 119 | 120 | 121 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/multiple-workers-one-file). -------------------------------------------------------------------------------- /docs/docs/examples/multiple-workers.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Multiple Workers 3 | sidebar_position: 10 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | It is possible for a single Piscina pool to run multiple workers at the same time. To do so, pass the worker `filename` to `run` method rather than to the Piscina constructor. 11 | 12 | To work with multiple workers using Typescript, check out [multiple workers in one file](./multiple-workers-one-file.mdx). 13 | 14 | ```javascript title="index.js" 15 | "use strict"; 16 | 17 | const Piscina = require("piscina"); 18 | const { resolve } = require("path"); 19 | 20 | const pool = new Piscina(); 21 | 22 | (async () => { 23 | console.log( 24 | await Promise.all([ 25 | pool.run({ a: 2, b: 3 }, { filename: resolve(__dirname, "add_worker") }), 26 | pool.run( 27 | { a: 2, b: 3 }, 28 | { filename: resolve(__dirname, "multiply_worker") } 29 | ), 30 | ]) 31 | ); 32 | })(); 33 | ``` 34 | 35 | Here are the two worker files used in this example: 36 | 37 | ```javascript title="add_worker.js" 38 | "use strict"; 39 | 40 | module.exports = ({ a, b }) => a + b; 41 | ``` 42 | 43 | ```javascript title="multiply_worker.js" 44 | "use strict"; 45 | 46 | module.exports = ({ a, b }) => a * b; 47 | ``` 48 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/multiple-workers). -------------------------------------------------------------------------------- /docs/docs/examples/n-api.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: N-API Native Addon 3 | sidebar_position: 11 4 | --- 5 | 6 | For CPU-intensive tasks or when implementing workers in languages such as C++ or Rust, you can leverage Piscina's support for native addons as worker implementations. 7 | 8 | ## Setup 9 | 10 | First, get the example from the [Piscina GitHub repository](https://github.com/piscinajs/piscina/tree/current/examples/n-api). 11 | 12 | Then install the dependencies: 13 | 14 | ```console 15 | npm i 16 | ``` 17 | 18 | Build the native addon artifacts: 19 | 20 | ```console 21 | npm run prebuild 22 | ``` 23 | 24 | The `prebuild` command will build the binary artifacts for the native 25 | addon and will put them in the `prebuilds` folder. Because of how 26 | prebuilds work, we need to use an intermediate JavaScript file to 27 | load and export them. For this example native addon, you'll find 28 | that in the `examples` folder. 29 | 30 | The `index.js` illustrates how to load and use the native addon as the 31 | worker implementation: 32 | 33 | ```js title="index.js" 34 | const Piscina = require('piscina'); 35 | const { resolve } = require('path'); 36 | 37 | const pool = new Piscina({ 38 | filename: resolve(__dirname, 'example') 39 | }); 40 | 41 | (async () => { 42 | // Submit 5 concurrent tasks 43 | console.log(await Promise.all([ 44 | pool.run(), 45 | pool.run(), 46 | pool.run(), 47 | pool.run(), 48 | pool.run() 49 | ])); 50 | })(); 51 | ``` 52 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/n-api). -------------------------------------------------------------------------------- /docs/docs/examples/named.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Named Tasks 3 | sidebar_position: 12 4 | --- 5 | 6 | import { WorkerWrapperComponent } from "@site/src/components/WorkerWrapper.mdx"; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | Piscina supports running named tasks within a single worker file. This example demonstrates how to use a dispatcher pattern to execute different operations based on the task name. 11 | 12 | 13 | 14 | 15 | ```javascript title="index.js" 16 | 'use strict'; 17 | 18 | const Piscina = require('piscina'); 19 | const { resolve } = require('path'); 20 | const { makeTask } = require('./helper'); 21 | 22 | const piscina = new Piscina({ 23 | filename: resolve(__dirname, 'worker.js') 24 | }); 25 | 26 | (async function () { 27 | const result = await Promise.all([ 28 | piscina.run(makeTask('add', 4, 6)), 29 | piscina.run(makeTask('sub', 4, 6)) 30 | ]); 31 | console.log(result); 32 | })(); 33 | ``` 34 | 35 | 36 | 37 | 38 | ```typescript title="index.ts" 39 | import Piscina from "piscina"; 40 | import { resolve } from "path"; 41 | import { filename } from "./worker"; 42 | import { makeTask } from "./helper"; 43 | 44 | const piscina = new Piscina({ 45 | filename: resolve(__dirname, "./workerWrapper.js"), 46 | workerData: { fullpath: filename }, 47 | }); 48 | (async function () { 49 | const result = await Promise.all([ 50 | piscina.run(makeTask('add', 4, 6)), 51 | piscina.run(makeTask('sub', 4, 6)) 52 | ]); 53 | console.log(result); 54 | })(); 55 | ``` 56 | 57 | 58 | 59 | 60 | The worker file uses a dispatcher to handle different operations: 61 | 62 | 63 | 64 | 65 | ```javascript title="worker.js" 66 | 'use strict'; 67 | 68 | const { dispatcher } = require('./helper'); 69 | 70 | module.exports = dispatcher({ 71 | add (a, b) { return a + b; }, 72 | sub (a, b) { return a - b; } 73 | }); 74 | ``` 75 | 76 | 77 | 78 | 79 | ```typescript title="worker.ts" 80 | import { dispatcher } from './helper'; 81 | 82 | export default dispatcher({ 83 | add (a: number, b: number): number { return a + b; }, 84 | sub (a: number, b: number): number { return a - b; } 85 | }); 86 | ``` 87 | 88 | 89 | 90 | 91 | The helper file provides utility functions for creating tasks and dispatching them: 92 | 93 | 94 | 95 | 96 | ```javascript title="helper.js" 97 | function makeTask (op, ...args) { 98 | return { op, args }; 99 | } 100 | 101 | function dispatcher (obj) { 102 | return async ({ op, args }) => { 103 | return await obj[op](...args); 104 | }; 105 | } 106 | 107 | module.exports = { 108 | dispatcher, 109 | makeTask 110 | }; 111 | ``` 112 | 113 | 114 | 115 | 116 | ```typescript title="helper.ts" 117 | type Task = { 118 | op: string; 119 | args: any[]; 120 | }; 121 | 122 | export function makeTask (op: string, ...args: any[]): Task { 123 | return { op, args }; 124 | } 125 | 126 | export function dispatcher (obj: Record) { 127 | return async ({ op, args }: Task) => { 128 | return await obj[op](...args); 129 | }; 130 | } 131 | ``` 132 | 133 | 134 | 135 | 136 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/named). -------------------------------------------------------------------------------- /docs/docs/examples/resource-limit.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Resource Limits 3 | sidebar_position: 15 4 | --- 5 | 6 | import Tabs from '@theme/Tabs'; 7 | import TabItem from '@theme/TabItem'; 8 | import { WorkerWrapperComponent } from '@site/src/components/WorkerWrapper.mdx'; 9 | 10 | Piscina allows you to set resource limits on worker threads to prevent them from consuming excessive memory. The example below will show you how to configure and use resource limits to handle out-of-memory scenarios. 11 | 12 | The main file sets up a Piscina instance with specific resource limits: 13 | 14 | - `maxOldGenerationSizeMb`: Limits the maximum size of the old generation in the V8 heap to 16 MB. 15 | - `maxYoungGenerationSizeMb`: Limits the maximum size of the young generation in the V8 heap to 4 MB. 16 | - `codeRangeSizeMb`: Limits the size of the code range to 16 MB. 17 | 18 | 19 | 20 | 21 | ```javascript title="index.js" 22 | 'use strict'; 23 | 24 | const Piscina = require('piscina'); 25 | const { resolve } = require('path'); 26 | const { strictEqual } = require('assert'); 27 | 28 | const piscina = new Piscina({ 29 | filename: resolve(__dirname, 'worker.js'), 30 | resourceLimits: { 31 | maxOldGenerationSizeMb: 16, 32 | maxYoungGenerationSizeMb: 4, 33 | codeRangeSizeMb: 16 34 | } 35 | }); 36 | 37 | (async function () { 38 | try { 39 | await piscina.run(); 40 | } catch (err) { 41 | console.log('Worker terminated due to resource limits'); 42 | strictEqual(err.code, 'ERR_WORKER_OUT_OF_MEMORY'); 43 | } 44 | })(); 45 | ``` 46 | 47 | 48 | 49 | 50 | ```typescript title="index.ts" 51 | import Piscina from 'piscina'; 52 | import { resolve } from 'path'; 53 | import { strictEqual } from 'assert'; 54 | import { filename } from './worker'; 55 | 56 | const piscina = new Piscina({ 57 | filename: resolve(__dirname, 'workerWrapper.js'), 58 | workerData: { fullpath: filename }, 59 | resourceLimits: { 60 | maxOldGenerationSizeMb: 16, 61 | maxYoungGenerationSizeMb: 4, 62 | codeRangeSizeMb: 16 63 | } 64 | }); 65 | 66 | (async function () { 67 | try { 68 | await piscina.run({}, { name: 'memoryLeak' }); 69 | } catch (err) { 70 | console.log('Worker terminated due to resource limits'); 71 | strictEqual((err as any).code, 'ERR_WORKER_OUT_OF_MEMORY'); 72 | } 73 | })(); 74 | ``` 75 | 76 | 77 | 78 | 79 | The worker file contains a function that deliberately causes a memory leak by creating an infinitely growing array. This will eventually exceed the memory limits set in the main file. 80 | 81 | 82 | 83 | 84 | ```javascript title="worker.js" 85 | 'use strict'; 86 | 87 | module.exports = () => { 88 | const array = []; 89 | while (true) { 90 | array.push([array]); 91 | } 92 | }; 93 | ``` 94 | 95 | 96 | 97 | ```typescript title="worker.ts" 98 | export const filename = __filename; 99 | 100 | export function memoryLeak(): void { 101 | const array: any[] = []; 102 | while (true) { 103 | array.push([array]); 104 | } 105 | } 106 | ``` 107 | 108 | 109 | 110 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/resourceLimits). -------------------------------------------------------------------------------- /docs/docs/examples/simple.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Simple 3 | sidebar_position: 19 4 | --- 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | import { WorkerWrapperComponent } from '@site/src/components/WorkerWrapper.mdx' 8 | 9 | In this example, we create a Piscina instance that uses a worker file to perform a simple addition operation. The main script (`index.js`) creates the Piscina instance and runs a task, while the worker script (`worker.js`) defines the task to be executed. 10 | 11 | 12 | 13 | 14 | ```javascript title="index.js" 15 | 'use strict'; 16 | 17 | const Piscina = require('../..'); 18 | const { resolve } = require('path'); 19 | 20 | const piscina = new Piscina({ 21 | filename: resolve(__dirname, 'worker.js') 22 | }); 23 | 24 | (async function () { 25 | const result = await piscina.run({ a: 4, b: 6 }); 26 | console.log(result); // Prints 10 27 | })(); 28 | ``` 29 | 30 | ```javascript title="worker.js" 31 | 'use strict'; 32 | 33 | module.exports = ({ a, b }) => { 34 | return a + b; 35 | }; 36 | ``` 37 | 38 | 39 | 40 | 41 | ```typescript title="index.ts" 42 | import Piscina from 'piscina'; 43 | import { resolve } from 'path'; 44 | import { filename } from './worker'; 45 | 46 | const piscina = new Piscina({ 47 | filename: resolve(__dirname, 'workerWrapper.js'), 48 | workerData: { fullpath: filename }, 49 | }); 50 | 51 | (async function () { 52 | const result = await piscina.run({ a: 4, b: 6 }); 53 | console.log(result); // Prints 10 54 | })(); 55 | ``` 56 | 57 | ```typescript title="worker.ts" 58 | export const filename = __filename; 59 | 60 | interface AdditionParams { 61 | a: number; 62 | b: number; 63 | } 64 | 65 | export default ({ a, b }: AdditionParams): number => { 66 | return a + b; 67 | }; 68 | ``` 69 | 70 | 71 | 72 | 73 | 74 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/simple). 75 | -------------------------------------------------------------------------------- /docs/docs/examples/simple_async.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Simple Async 3 | sidebar_position: 18 4 | --- 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | import { WorkerWrapperComponent } from '@site/src/components/WorkerWrapper.mdx' 8 | 9 | This example builds upon the [simple addition](./simple.mdx) scenario in the previous section. In this section, we simulated an asynchronous operation in the worker file. The simulated delay (100ms) represents any asynchronous operation that might occur in a real-world scenario, such as database queries, file I/O, or network requests. 10 | 11 | 12 | 13 | 14 | ```javascript title="index.js" 15 | 'use strict'; 16 | 17 | const Piscina = require('../..'); 18 | const { resolve } = require('path'); 19 | 20 | const piscina = new Piscina({ 21 | filename: resolve(__dirname, 'worker.js') 22 | }); 23 | 24 | (async function () { 25 | const result = await piscina.run({ a: 4, b: 6 }); 26 | console.log(result); // Prints 10 27 | })(); 28 | ``` 29 | 30 | ```javascript title="worker.js" 31 | 'use strict'; 32 | 33 | const { promisify } = require('util'); 34 | const sleep = promisify(setTimeout); 35 | 36 | module.exports = async ({ a, b }) => { 37 | // Fake some async activity 38 | await sleep(100); 39 | return a + b; 40 | }; 41 | ``` 42 | 43 | 44 | 45 | 46 | ```typescript title="index.ts" 47 | import Piscina from 'piscina'; 48 | import { resolve } from 'path'; 49 | import { filename } from './worker'; 50 | 51 | const piscina = new Piscina({ 52 | filename: resolve(__dirname, 'workerWrapper.js'), 53 | workerData: { fullpath: filename }, 54 | }); 55 | 56 | (async function () { 57 | const result = await piscina.run({ a: 4, b: 6 }); 58 | console.log(result); // Prints 10 59 | })(); 60 | ``` 61 | 62 | ```typescript title="worker.ts" 63 | import { promisify } from 'util'; 64 | 65 | const sleep = promisify(setTimeout); 66 | 67 | interface AdditionParams { 68 | a: number; 69 | b: number; 70 | } 71 | 72 | export default async ({ a, b }: AdditionParams): Promise => { 73 | // Fake some async activity 74 | await sleep(100); 75 | return a + b; 76 | }; 77 | export const filename = __filename; 78 | ``` 79 | 80 | 81 | 82 | 83 | 84 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/simple_async). 85 | -------------------------------------------------------------------------------- /docs/docs/examples/webstreams-transform.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Web Streams Transfer 3 | sidebar_position: 24 4 | --- 5 | 6 | import {WorkerWrapperComponent} from '@site/src/components/WorkerWrapper.mdx'; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | 11 | Using [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) and Piscina, you can create a data processing pipeline. This is useful for cases involving real-time data transformation or analysis. 12 | 13 | In the main script below, we set up three key components of the Web Streams API: 14 | 15 | * A `ReadableStream` that generates a sequence of numbers from 1 to 9, and then 0. 16 | * A `WritableStream` that simply logs each chunk it receives to the console. 17 | * A `TransformStream` that processes each chunk using a Piscina worker pool. 18 | 19 | The TransformStream's `transform` method uses `pool.run()` to process each chunk in a worker thread. This allows for parallel processing of the stream data. 20 | 21 | Finally, the script sets up a pipeline using `pipeThrough()` and `pipeTo()` methods, connecting the `ReadableStream` to the `TransformStream`, and then to the `WritableStream`. 22 | 23 | 24 | 25 | 26 | ```javascript title="worker.mjs" 27 | export default async function (num) { 28 | return 'ABC'.repeat(num * num); 29 | } 30 | ``` 31 | 32 | ```javascript title="index.mjs" 33 | import Piscina from 'piscina'; 34 | import { 35 | ReadableStream, 36 | TransformStream, 37 | WritableStream 38 | } from 'node:stream/web'; 39 | 40 | const pool = new Piscina({ 41 | filename: new URL('./worker.mjs', import.meta.url).href 42 | }); 43 | 44 | const readable = new ReadableStream({ 45 | start () { 46 | this.chunks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; 47 | }, 48 | 49 | pull (controller) { 50 | const chunk = this.chunks.shift(); 51 | controller.enqueue(chunk); 52 | if (this.chunks.length === 0) { 53 | controller.close(); 54 | } 55 | } 56 | }); 57 | 58 | const writable = new WritableStream({ 59 | write (chunk) { 60 | console.log(chunk); 61 | } 62 | }); 63 | 64 | const transform = new TransformStream({ 65 | async transform (chunk, controller) { 66 | controller.enqueue(await pool.run(chunk)); 67 | } 68 | }); 69 | 70 | readable.pipeThrough(transform).pipeTo(writable); 71 | ``` 72 | 73 | 74 | 75 | 76 | ```typescript title="worker.ts" 77 | export default async function (num: number): Promise { 78 | return 'ABC'.repeat(num * num); 79 | } 80 | export const filename = __filename; 81 | ``` 82 | 83 | ```typescript title="index.ts" 84 | import { resolve } from 'path'; 85 | import Piscina from 'piscina'; 86 | import { 87 | ReadableStream, 88 | WritableStream 89 | } from 'node:stream/web'; 90 | import { filename } from './worker'; 91 | 92 | const pool = new Piscina({ 93 | filename: resolve(__dirname, 'workerWrapper.js'), 94 | workerData: { fullpath: filename }, 95 | }); 96 | 97 | const readable = new ReadableStream({ 98 | start () { 99 | this.chunks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; 100 | }, 101 | 102 | pull (controller) { 103 | const chunk = this.chunks.shift(); 104 | controller.enqueue(chunk); 105 | if (this.chunks.length === 0) { 106 | controller.close(); 107 | } 108 | } 109 | }); 110 | 111 | const writable = new WritableStream({ 112 | write (chunk) { 113 | console.log(chunk); 114 | } 115 | }); 116 | 117 | const transform = new TransformStream({ 118 | async transform (chunk, controller) { 119 | controller.enqueue(await pool.run(chunk)); 120 | } 121 | }); 122 | 123 | readable.pipeThrough(transform).pipeTo(writable); 124 | ``` 125 | 126 | 127 | 128 | 129 | 130 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/webstreams-transform). -------------------------------------------------------------------------------- /docs/docs/examples/webstreams.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Web Streams 3 | sidebar_position: 25 4 | --- 5 | 6 | import {WorkerWrapperComponent} from '@site/src/components/WorkerWrapper.mdx'; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | You can work with modern Web APIs like [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) using Piscina. Web Streams enable efficient processing of streaming data across multiple threads. It's particularly useful for scenarios involving large datasets or real-time data processing where the benefits of multi-threading can be significant. 11 | 12 | In the main script (`index.mjs`), we create a Piscina instance and set up a `ReadableStream` that generates a sequence of numbers. We also create a `WritableStream` that simply logs each chunk it receives. The script then runs a task in the worker pool, passing both streams as arguments. 13 | 14 | The worker script (`worker.mjs`) defines an async function that reads from the provided `ReadableStream` and writes to the `WritableStream`. It uses a for-await loop to iterate over the chunks in the `ReadableStream`, writing each chunk to the `WritableStream`. 15 | 16 | 17 | 18 | 19 | ```javascript title="worker.mjs" 20 | export default async function ({ readable, writable }) { 21 | const writer = writable.getWriter(); 22 | for await (const chunk of readable) { 23 | await writer.write(chunk); 24 | } 25 | writer.close(); 26 | } 27 | ``` 28 | 29 | ```javascript title="index.mjs" 30 | import Piscina from 'piscina'; 31 | import { 32 | ReadableStream, 33 | WritableStream 34 | } from 'node:stream/web'; 35 | 36 | const pool = new Piscina({ 37 | filename: new URL('./worker.mjs', import.meta.url).href 38 | }); 39 | 40 | const readable = new ReadableStream({ 41 | start () { 42 | this.chunks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; 43 | }, 44 | 45 | pull (controller) { 46 | const chunk = this.chunks.shift(); 47 | controller.enqueue(chunk); 48 | if (this.chunks.length === 0) { 49 | controller.close(); 50 | } 51 | } 52 | }); 53 | 54 | const writable = new WritableStream({ 55 | write (chunk) { 56 | console.log(chunk); 57 | } 58 | }); 59 | 60 | (async function () { 61 | await pool.run({ readable, writable }, { transferList: [readable, writable] }); 62 | })() 63 | 64 | ``` 65 | 66 | 67 | 68 | 69 | ```typescript title="worker.ts" 70 | import { ReadableStream, WritableStream } from 'node:stream/web'; 71 | 72 | export default async function ({ readable, writable }: { readable: ReadableStream, writable: WritableStream }): Promise { 73 | const writer = writable.getWriter(); 74 | for await (const chunk of readable) { 75 | await writer.write(chunk); 76 | } 77 | writer.close(); 78 | } 79 | ``` 80 | 81 | ```typescript title="index.ts" 82 | import { resolve } from 'path'; 83 | import Piscina from 'piscina'; 84 | import { 85 | ReadableStream, 86 | WritableStream 87 | } from 'node:stream/web'; 88 | import { filename } from './worker'; 89 | 90 | const pool = new Piscina({ 91 | filename: resolve(__dirname, 'workerWrapper.js'), 92 | workerData: { fullpath: filename }, 93 | }); 94 | 95 | const readable = new ReadableStream({ 96 | start () { 97 | this.chunks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; 98 | }, 99 | 100 | pull (controller) { 101 | const chunk = this.chunks.shift(); 102 | controller.enqueue(chunk); 103 | if (this.chunks.length === 0) { 104 | controller.close(); 105 | } 106 | } 107 | }); 108 | 109 | const writable = new WritableStream({ 110 | write (chunk) { 111 | console.log(chunk); 112 | } 113 | }); 114 | (async function () { 115 | await pool.run({ readable, writable }, { transferList: [readable, writable] }); 116 | })() 117 | ``` 118 | 119 | 120 | 121 | 122 | 123 | A key aspect of this example is the use of the `transferList` option when running the task. This allows the `ReadableStream` and `WritableStream` instances to be transferred to the worker thread, rather than cloned. This is crucial for maintaining the integrity of the streams across threads. 124 | 125 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/webstreams). -------------------------------------------------------------------------------- /docs/docs/examples/worker-options.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Worker Options 3 | sidebar_position: 26 4 | --- 5 | 6 | import {WorkerWrapperComponent} from '@site/src/components/WorkerWrapper.mdx'; 7 | import Tabs from "@theme/Tabs"; 8 | import TabItem from "@theme/TabItem"; 9 | 10 | 11 | Piscina allows you to customize the environment and runtime options for worker threads. You can set environment variables, command-line arguments, and other options for the worker processes. 12 | 13 | This is useful when you need to: 14 | 15 | - Pass configuration or environment-specific data to workers 16 | - Control Node.js runtime behavior in workers 17 | - Provide initial data to workers without including it in every task 18 | 19 | 20 | 21 | 22 | ```javascript title="index.js" 23 | 'use strict'; 24 | 25 | const Piscina = require('../..'); 26 | const { resolve } = require('path'); 27 | 28 | const piscina = new Piscina({ 29 | filename: resolve(__dirname, 'worker.js'), 30 | env: { A: '123' }, 31 | argv: ['a', 'b', 'c'], 32 | execArgv: ['--no-warnings'], 33 | workerData: 'ABC' 34 | }); 35 | 36 | (async function () { 37 | const result = await piscina.run({ a: 4, b: 6 }); 38 | console.log(result); // Prints 10 39 | })(); 40 | ``` 41 | 42 | ```javascript title="worker.js" 43 | 'use strict'; 44 | 45 | const Piscina = require('../..'); 46 | const { format } = require('util'); 47 | 48 | module.exports = ({ a, b }) => { 49 | console.log(` 50 | process.argv: ${process.argv.slice(2)} 51 | process.execArgv: ${process.execArgv} 52 | process.env: ${format({ ...process.env })} 53 | workerData: ${Piscina.workerData}`); 54 | return a + b; 55 | }; 56 | ``` 57 | 58 | 59 | 60 | 61 | ```typescript title="index.ts" 62 | import { resolve } from 'path'; 63 | import Piscina from 'piscina'; 64 | import { filename } from './worker'; 65 | 66 | const piscina = new Piscina({ 67 | filename: resolve(__dirname, 'workerWrapper.js'), 68 | workerData: { fullpath: filename }, 69 | env: { A: '123' }, 70 | argv: ['a', 'b', 'c'], 71 | execArgv: ['--no-warnings'], 72 | }); 73 | 74 | (async function () { 75 | const result = await piscina.run({ a: 4, b: 6 }); 76 | console.log(result); // Prints 10 77 | })(); 78 | ``` 79 | 80 | ```typescript title="worker.ts" 81 | import Piscina from 'piscina'; 82 | import { format } from 'util'; 83 | 84 | export default ({ a, b }: { a: number, b: number }): number => { 85 | console.log(` 86 | process.argv: ${process.argv.slice(2)} 87 | process.execArgv: ${process.execArgv} 88 | process.env: ${format({ ...process.env })} 89 | workerData: ${Piscina.workerData}`); 90 | return a + b; 91 | }; 92 | ``` 93 | 94 | 95 | 96 | 97 | 98 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/worker_options). -------------------------------------------------------------------------------- /docs/docs/getting-started/Installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Installation 3 | sidebar_position: 1 4 | --- 5 | import Tabs from "@theme/Tabs"; 6 | import TabItem from "@theme/TabItem"; 7 | 8 | This section will guide you through the process of getting started with Piscina.js. We'll cover installation, basic usage, and configuration options to help you quickly integrate Piscina.js into your project. 9 | 10 | To get started, you'll need to have Node.js version 16.x or higher installed on your system. Open your terminal and run the following command: 11 | 12 | 13 | 14 | ```bash tab={"label":"npm"} 15 | npm install piscina 16 | ``` 17 | 18 | 19 | ```bash tab={"label":"yarn"} 20 | yarn add piscina 21 | ``` 22 | 23 | 24 | ```bash tab={"label":"pnpm"} 25 | pnpm add piscina 26 | ``` 27 | 28 | 29 | ```bash tab={"label":"Bun"} 30 | bun add piscina 31 | ``` 32 | 33 | 34 | 35 | :::note 36 | While Bun can be used to install Piscina, its behavior while running within Bun is not assured. 37 | ::: 38 | This will download and install the latest version of Piscina.js and its dependencies. 39 | Once you have Piscina.js installed, you can start using it in your code. -------------------------------------------------------------------------------- /docs/docs/getting-started/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting Started", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn how to install and use Piscina in your application." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/getting-started/typescript.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: Typescript 3 | sidebar_position: 4 4 | --- 5 | import {WorkerWrapperComponent} from '@site/src/components/WorkerWrapper.mdx'; 6 | 7 | Although Piscina itself is written in TypeScript and supports TypeScript out of the box, complication arises when trying to use `.ts` files directly as worker files because Node.js does not support TypeScript natively. 8 | 9 | To work around this, you would typically need to compile your TypeScript worker files to JavaScript first, and then point Piscina’s `filename` option to the compiled JavaScript files. Consider the following methods: 10 | 11 | ## Method 1: Using a Worker Wrapper File and `ts_node` 12 | 13 | The worker wrapper checks if the provided file path ends with `.ts` and registers the `ts-node` compiler to handle TypeScript files. 14 | 15 | 16 | 17 | In your `worker.ts`: 18 | 19 | ```typescript title='worker.ts' 20 | 21 | import { resolve } from 'path'; 22 | 23 | export const filename = resolve(__filename); 24 | 25 | interface Inputs { 26 | a: number; 27 | b: number; 28 | } 29 | 30 | export function addNumbers({ a, b }: Inputs): number { 31 | return a + b; 32 | } 33 | ``` 34 | 35 | Inside the main application: 36 | 37 | ```typescript title='main.ts' 38 | 39 | import Piscina from 'piscina'; 40 | import { resolve } from 'path'; 41 | import { filename } from './worker'; 42 | 43 | const piscina = new Piscina({ 44 | filename: resolve(__dirname, './workerWrapper.js'), 45 | workerData: { fullpath: filename }, 46 | }); 47 | 48 | (async () => { 49 | const result = await piscina.run({ a: 2, b: 3 }, { name: 'addNumbers' }); 50 | console.log('Result:', result); 51 | })(); 52 | ``` 53 | 54 | ## Method 2: Inline Worker Code 55 | 56 | Alternatively, you can include the worker code in the same file as your main code and use the `isMainThread` flag from `worker_threads` to determine whether the code is running in the main thread or the worker thread. 57 | 58 | ```typescript title="main.ts" 59 | 60 | import Piscina from 'piscina'; 61 | import { isMainThread } from 'worker_threads'; 62 | 63 | interface Inputs { 64 | a: number; 65 | b: number; 66 | } 67 | 68 | if (isMainThread) { 69 | const piscina = new Piscina({ filename: __filename }); 70 | 71 | (async () => { 72 | const task: Inputs = { a: 1, b: 2 }; 73 | console.log(await piscina.run(task)); 74 | })(); 75 | } else { 76 | export default ({ a, b }: Inputs): number => { 77 | return a + b; 78 | }; 79 | } 80 | ``` 81 | You can also check out this example on [github](https://github.com/piscinajs/piscina/tree/current/examples/typescript). -------------------------------------------------------------------------------- /docs/docs/update-log/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Update Log", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "This section helps you stay updated about latest changes and bug fixes." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/update-log/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ChangeLog" 3 | sidebar_position: 1 4 | --- 5 | 6 | import Changelog from '../../../CHANGELOG.md' 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/docs/update-log/release-notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Release Notes 3 | sidebar_position: 1 4 | --- 5 | 6 | ### 4.1.0 7 | 8 | #### Features 9 | 10 | * add `needsDrain` property ([#368](https://github.com/piscinajs/piscina/issues/368)) ([2d49b63](https://github.com/piscinajs/piscina/commit/2d49b63368116c172a52e2019648049b4d280162)) 11 | * correctly handle process.exit calls outside of a task ([#361](https://github.com/piscinajs/piscina/issues/361)) ([8e6d16e](https://github.com/piscinajs/piscina/commit/8e6d16e1dc23f8bb39772ed954f6689852ad435f)) 12 | 13 | 14 | #### Bug Fixes 15 | 16 | * Fix types for TypeScript 4.7 ([#239](https://github.com/piscinajs/piscina/issues/239)) ([a38fb29](https://github.com/piscinajs/piscina/commit/a38fb292e8fcc45cc20abab8668f82d908a24dc0)) 17 | * use CJS imports ([#374](https://github.com/piscinajs/piscina/issues/374)) ([edf8dc4](https://github.com/piscinajs/piscina/commit/edf8dc4f1a19e9b49e266109cdb70d9acc86f3ca)) 18 | 19 | ### 4.0.0 20 | 21 | * Drop Node.js 14.x support 22 | * Add Node.js 20.x to CI 23 | 24 | ### 3.2.0 25 | 26 | * Adds a new `PISCINA_DISABLE_ATOMICS` environment variable as an alternative way of 27 | disabling Piscina's internal use of the `Atomics` API. (https://github.com/piscinajs/piscina/pull/163) 28 | * Fixes a bug with transferable objects. (https://github.com/piscinajs/piscina/pull/155) 29 | * Fixes CI issues with TypeScript. (https://github.com/piscinajs/piscina/pull/161) 30 | 31 | ### 3.1.0 32 | 33 | * Deprecates `piscina.runTask()`; adds `piscina.run()` as an alternative. 34 | https://github.com/piscinajs/piscina/commit/d7fa24d7515789001f7237ad6ae9ad42d582fc75 35 | * Allows multiple exported handler functions from a single file. 36 | https://github.com/piscinajs/piscina/commit/d7fa24d7515789001f7237ad6ae9ad42d582fc75 37 | 38 | ### 3.0.0 39 | 40 | * Drops Node.js 10.x support 41 | * Updates minimum TypeScript target to ES2019 42 | 43 | ### 2.1.0 44 | 45 | * Adds name property to indicate `AbortError` when tasks are 46 | canceled using an `AbortController` (or similar) 47 | * More examples 48 | 49 | ### 2.0.0 50 | 51 | * Added unmanaged file descriptor tracking 52 | * Updated dependencies 53 | 54 | ### 1.6.1 55 | 56 | * Bug fix: Reject if AbortSignal is already aborted 57 | * Bug Fix: Use once listener for abort event 58 | 59 | ### 1.6.0 60 | 61 | * Add the `niceIncrement` configuration parameter. 62 | 63 | ### 1.5.1 64 | 65 | * Bug fixes around abortable task selection. 66 | 67 | ### 1.5.0 68 | 69 | * Added `Piscina.move()` 70 | * Added Custom Task Queues 71 | * Added utilization metric 72 | * Wait for workers to be ready before considering them as candidates 73 | * Additional examples 74 | 75 | ### 1.4.0 76 | 77 | * Added `maxQueue = 'auto'` to autocalculate the maximum queue size. 78 | * Added more examples, including an example of implementing a worker 79 | as a Node.js native addon. 80 | 81 | ### 1.3.0 82 | 83 | * Added the `'drain'` event 84 | 85 | ### 1.2.0 86 | 87 | * Added support for ESM and file:// URLs 88 | * Added `env`, `argv`, `execArgv`, and `workerData` options 89 | * More examples 90 | 91 | ### 1.1.0 92 | 93 | * Added support for Worker Thread `resourceLimits` 94 | 95 | ### 1.0.0 96 | 97 | * Initial release! 98 | -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import { themes as prismThemes } from 'prism-react-renderer'; 2 | import type { Config } from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | 5 | const config: Config = { 6 | title: 'Piscina', 7 | tagline: 'The node.js worker pool', 8 | favicon: 'img/favicon.ico', 9 | 10 | // Set the production url of your site here 11 | url: 'https://piscinajs.dev', 12 | // Set the // pathname under which your site is served 13 | // For GitHub pages deployment, it is often '//' 14 | baseUrl: '/', 15 | 16 | // GitHub pages deployment config. 17 | // If you aren't using GitHub pages, you don't need these. 18 | organizationName: 'Piscinajs', // Usually your GitHub org/user name. 19 | projectName: 'piscina', // Usually your repo name. 20 | 21 | onBrokenLinks: 'throw', 22 | onBrokenMarkdownLinks: 'warn', 23 | 24 | // Even if you don't use internationalization, you can use this field to set 25 | // useful metadata like html lang. For example, if your site is Chinese, you 26 | // may want to replace "en" with "zh-Hans". 27 | i18n: { 28 | defaultLocale: 'en', 29 | locales: ['en'] 30 | }, 31 | 32 | presets: [ 33 | [ 34 | 'classic', 35 | { 36 | docs: { 37 | sidebarPath: './sidebars.ts', 38 | routeBasePath: '/' 39 | }, 40 | blog: false, 41 | theme: { 42 | customCss: './src/css/custom.css' 43 | } 44 | } satisfies Preset.Options 45 | ] 46 | ], 47 | 48 | themes: [ 49 | [ 50 | require.resolve('@easyops-cn/docusaurus-search-local'), 51 | { 52 | hashed: true, 53 | docsRouteBasePath: '/', 54 | searchBarPosition: 'right' 55 | } 56 | ] 57 | ], 58 | themeConfig: { 59 | // Replace with your project's social card 60 | image: 'img/banner.jpg', 61 | navbar: { 62 | title: 'Piscina', 63 | logo: { 64 | alt: 'My Site Logo', 65 | src: 'img/logo.png' 66 | }, 67 | items: [ 68 | { 69 | type: 'docSidebar', 70 | sidebarId: 'tutorialSidebar', 71 | position: 'left', 72 | label: 'Documentation' 73 | }, 74 | // { 75 | // type: 'search', 76 | // position: 'right', 77 | // }, 78 | 79 | { 80 | href: 'https://github.com/piscinajs/piscina', 81 | label: 'GitHub', 82 | position: 'right' 83 | } 84 | ] 85 | }, 86 | footer: { 87 | style: 'dark', 88 | links: [ 89 | { 90 | title: 'Quick Start', 91 | items: [ 92 | { 93 | label: 'Getting started', 94 | to: '/getting-started/Installation' 95 | } 96 | ] 97 | }, 98 | { 99 | title: 'Community', 100 | items: [ 101 | { 102 | label: 'Github', 103 | href: 'https://github.com/piscinajs/piscina' 104 | } 105 | ] 106 | } 107 | ], 108 | copyright: `Copyright © ${new Date().getFullYear()} Piscina. Built with Docusaurus.` 109 | }, 110 | prism: { 111 | theme: prismThemes.github, 112 | darkTheme: prismThemes.dracula 113 | } 114 | } satisfies Preset.ThemeConfig 115 | }; 116 | 117 | export default config; 118 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.4.0", 19 | "@docusaurus/preset-classic": "3.4.0", 20 | "@easyops-cn/docusaurus-search-local": "^0.44.0", 21 | "@mdx-js/react": "^3.0.0", 22 | "clsx": "^2.0.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^18.0.0", 25 | "react-dom": "^18.0.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.4.0", 29 | "@docusaurus/tsconfig": "3.4.0", 30 | "@docusaurus/types": "3.4.0", 31 | "typescript": "~5.2.2" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 3 chrome version", 41 | "last 3 firefox version", 42 | "last 5 safari version" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=18.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }] 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /docs/src/components/WorkerWrapper.mdx: -------------------------------------------------------------------------------- 1 | // WorkerWrapperComponent.mdx 2 | import React from 'react'; 3 | 4 | export const WorkerWrapperComponent = () => ( 5 |
 6 |     {`const { workerData } = require('worker_threads');
 7 | 
 8 | if (workerData.fullpath.endsWith(".ts")) {
 9 | require("ts-node").register();
10 | }
11 | module.exports = require(workerData.fullpath);
12 | `}
13 | 
14 |   
15 | ); 16 | 17 | export default WorkerWrapperComponent; 18 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme="dark"] { 22 | --ifm-color-primary: #1eb2e5; 23 | --ifm-color-primary-dark: #1eb2e5; 24 | --ifm-color-primary-darker: #1eb2e5; 25 | --ifm-color-primary-darkest: #1eb2e5; 26 | --ifm-color-primary-light: #1eb2e5; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | background-color: #03091f; 31 | } 32 | 33 | p { 34 | font-size: 17px; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piscinajs/piscina/039cb7bf302849f9cf01bb493676e997cf2f6b00/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piscinajs/piscina/039cb7bf302849f9cf01bb493676e997cf2f6b00/docs/static/img/banner.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piscinajs/piscina/039cb7bf302849f9cf01bb493676e997cf2f6b00/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piscinajs/piscina/039cb7bf302849f9cf01bb493676e997cf2f6b00/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piscinajs/piscina/039cb7bf302849f9cf01bb493676e997cf2f6b00/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docusaurus/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('neostandard')({ 4 | semi: true, 5 | ts: true, 6 | noStyle: true, 7 | ignores: ['dist', 'node_modules', 'docs/build', 'docs/.docusaurus'], 8 | globals: { 9 | SharedArrayBuffer: true, 10 | Atomics: true, 11 | AbortController: true, 12 | MessageChannel: true, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /examples/abort/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { AbortController } = require('abort-controller'); 5 | const { resolve } = require('path'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js') 9 | }); 10 | 11 | (async function () { 12 | const abortController = new AbortController(); 13 | try { 14 | const task = piscina.run( 15 | { a: 4, b: 6 }, 16 | { signal: abortController.signal }); 17 | abortController.abort(); 18 | await task; 19 | } catch (err) { 20 | console.log('The task was cancelled'); 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /examples/abort/index2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const EventEmitter = require('events'); 5 | const { resolve } = require('path'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js') 9 | }); 10 | 11 | (async function () { 12 | const ee = new EventEmitter(); 13 | try { 14 | const task = piscina.run({ a: 4, b: 6 }, { signal: ee }); 15 | ee.emit('abort'); 16 | await task; 17 | } catch (err) { 18 | console.log('The task was cancelled'); 19 | } 20 | })(); 21 | -------------------------------------------------------------------------------- /examples/abort/index3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const EventEmitter = require('events'); 5 | const { resolve } = require('path'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js') 9 | }); 10 | 11 | (async function () { 12 | const ee = new EventEmitter(); 13 | // Use a timer to limit task processing length 14 | const t = setTimeout(() => ee.emit('abort'), 500); 15 | try { 16 | await piscina.run({ a: 4, b: 6 }, { signal: ee }); 17 | } catch (err) { 18 | console.log('The task timed out'); 19 | } finally { 20 | // Clear the timeout in a finally to make sure 21 | // it is definitely cleared. 22 | clearTimeout(t); 23 | } 24 | })(); 25 | -------------------------------------------------------------------------------- /examples/abort/index4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | 6 | const piscina = new Piscina({ 7 | filename: resolve(__dirname, 'worker.js') 8 | }); 9 | 10 | (async function () { 11 | // Using the new built-in Node.js AbortController 12 | // Node.js version 15.0 or higher 13 | 14 | const ac = new AbortController(); 15 | 16 | // Use a timer to limit task processing length 17 | const t = setTimeout(() => ac.abort(), 500); 18 | try { 19 | await piscina.run({ a: 4, b: 6 }, { signal: ac.signal }); 20 | } catch (err) { 21 | console.log('The task timed out'); 22 | } finally { 23 | // Clear the timeout in a finally to make sure 24 | // it is definitely cleared. 25 | clearTimeout(t); 26 | } 27 | })(); 28 | -------------------------------------------------------------------------------- /examples/abort/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | module.exports = async ({ a, b }) => { 7 | await sleep(10000); 8 | return a + b; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/async_load/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | 5 | const pool = new Piscina(); 6 | const { resolve } = require('path'); 7 | 8 | (async () => { 9 | await Promise.all([ 10 | pool.run({}, { filename: resolve(__dirname, 'worker') }), 11 | pool.run({}, { filename: resolve(__dirname, 'worker.mjs') }) 12 | ]); 13 | })(); 14 | -------------------------------------------------------------------------------- /examples/async_load/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | module.exports = (async () => { 7 | await sleep(500); 8 | return () => console.log('hello from an async loaded CommonJS worker'); 9 | })(); 10 | -------------------------------------------------------------------------------- /examples/async_load/worker.mjs: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | const sleep = util.promisify(setTimeout); 3 | 4 | async function load () { 5 | await sleep(500); 6 | return () => console.log('hello from an async loaded ESM worker'); 7 | } 8 | 9 | export default load(); 10 | -------------------------------------------------------------------------------- /examples/broadcast/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { BroadcastChannel } = require('worker_threads'); 4 | const { resolve } = require('path'); 5 | 6 | const Piscina = require('piscina'); 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js'), 9 | // Set atomics to disabled to avoid threads being blocked when idle 10 | atomics: 'disabled' 11 | }); 12 | 13 | async function main () { 14 | const bc = new BroadcastChannel('my_channel'); 15 | // start worker 16 | Promise.all([ 17 | piscina.run('thread 1'), 18 | piscina.run('thread 2') 19 | ]); 20 | // post message in one second 21 | setTimeout(() => { 22 | bc.postMessage('Main thread message'); 23 | }, 1000); 24 | } 25 | 26 | main(); 27 | -------------------------------------------------------------------------------- /examples/broadcast/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { BroadcastChannel } = require('worker_threads'); 3 | 4 | module.exports = async (thread) => { 5 | const bc = new BroadcastChannel('my_channel'); 6 | bc.onmessage = (event) => { 7 | console.log(thread + ' Received from:' + event.data); 8 | }; 9 | await new Promise((resolve) => { 10 | setTimeout(resolve, 2000); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /examples/es-module/index.mjs: -------------------------------------------------------------------------------- 1 | import { Piscina } from 'piscina'; 2 | 3 | const piscina = new Piscina({ 4 | filename: new URL('./worker.mjs', import.meta.url).href 5 | }); 6 | 7 | const result = await piscina.run({ a: 4, b: 6 }); 8 | console.log(result); // Prints 10 9 | -------------------------------------------------------------------------------- /examples/es-module/worker.mjs: -------------------------------------------------------------------------------- 1 | export default ({ a, b }) => { 2 | return a + b; 3 | }; 4 | -------------------------------------------------------------------------------- /examples/message_port/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../../dist/src'); 4 | const { resolve } = require('path'); 5 | const { MessageChannel } = require('worker_threads'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js') 9 | }); 10 | 11 | (async function () { 12 | const channel = new MessageChannel(); 13 | channel.port2.on('message', (message) => { 14 | console.log(message); 15 | channel.port2.close(); 16 | }); 17 | await piscina.run({ port: channel.port1 }, { transferList: [channel.port1] }); 18 | })(); 19 | -------------------------------------------------------------------------------- /examples/message_port/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ port }) => { 4 | port.postMessage('hello from the worker pool'); 5 | }; 6 | -------------------------------------------------------------------------------- /examples/messages/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../../dist/src'); 4 | const { resolve } = require('path'); 5 | 6 | const piscina = new Piscina({ 7 | filename: resolve(__dirname, 'worker.js') 8 | }); 9 | 10 | (async function () { 11 | piscina.on('message', (event) => { 12 | console.log('Messsage received from worker: ', event); 13 | }); 14 | 15 | await piscina.run(); 16 | })(); 17 | -------------------------------------------------------------------------------- /examples/messages/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { parentPort } = require('worker_threads'); 3 | 4 | module.exports = () => { 5 | parentPort.postMessage('hello from the worker pool'); 6 | }; 7 | -------------------------------------------------------------------------------- /examples/move/index.js: -------------------------------------------------------------------------------- 1 | const Piscina = require('../..'); 2 | const { resolve } = require('path'); 3 | 4 | const pool = new Piscina({ 5 | filename: resolve(__dirname, 'worker.js'), 6 | idleTimeout: 1000 7 | }); 8 | 9 | (async () => { 10 | // The task will transfer an ArrayBuffer 11 | // back to the main thread rather than 12 | // cloning it. 13 | const u8 = await pool.run(Piscina.move(new Uint8Array(2))); 14 | console.log(u8.length); 15 | })(); 16 | -------------------------------------------------------------------------------- /examples/move/worker.js: -------------------------------------------------------------------------------- 1 | const { move } = require('../..'); 2 | 3 | module.exports = () => { 4 | // Using move causes the Uint8Array to be 5 | // transferred rather than cloned. 6 | return move(new Uint8Array(10)); 7 | }; 8 | -------------------------------------------------------------------------------- /examples/multiple-workers-one-file/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | 6 | // It is possible for a single Piscina pool to run multiple 7 | // workers at the same time. To do so, pass the worker filename 8 | // to runTask rather than to the Piscina constructor. 9 | 10 | const pool = new Piscina({ filename: resolve(__dirname, 'worker.js') }); 11 | 12 | (async () => { 13 | console.log(await Promise.all([ 14 | pool.run({ a: 2, b: 3 }, { name: 'add' }), 15 | pool.run({ a: 2, b: 3 }, { name: 'multiply' }) 16 | ])); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/multiple-workers-one-file/index.mjs: -------------------------------------------------------------------------------- 1 | import { Piscina } from 'piscina'; 2 | 3 | const pool = new Piscina({ 4 | filename: new URL('./worker.mjs', import.meta.url).href 5 | }); 6 | 7 | console.log(await Promise.all([ 8 | pool.run({ a: 2, b: 3 }, { name: 'add' }), 9 | pool.run({ a: 2, b: 3 }, { name: 'multiply' }) 10 | ])); 11 | -------------------------------------------------------------------------------- /examples/multiple-workers-one-file/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function add ({ a, b }) { return a + b; } 4 | function multiply ({ a, b }) { return a * b; } 5 | 6 | add.add = add; 7 | add.multiply = multiply; 8 | 9 | // The add function is the default handler, but 10 | // additional named handlers are exported. 11 | module.exports = add; 12 | -------------------------------------------------------------------------------- /examples/multiple-workers-one-file/worker.mjs: -------------------------------------------------------------------------------- 1 | export function add ({ a, b }) { return a + b; } 2 | export function multiply ({ a, b }) { return a * b; }; 3 | 4 | export default add; 5 | -------------------------------------------------------------------------------- /examples/multiple-workers/add_worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ a, b }) => a + b; 4 | -------------------------------------------------------------------------------- /examples/multiple-workers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | 6 | // It is possible for a single Piscina pool to run multiple 7 | // workers at the same time. To do so, pass the worker filename 8 | // to runTask rather than to the Piscina constructor. 9 | 10 | const pool = new Piscina(); 11 | 12 | (async () => { 13 | console.log(await Promise.all([ 14 | pool.run({ a: 2, b: 3 }, { filename: resolve(__dirname, 'add_worker') }), 15 | pool.run({ a: 2, b: 3 }, { filename: resolve(__dirname, 'multiply_worker') }) 16 | ])); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/multiple-workers/multiply_worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ a, b }) => a * b; 4 | -------------------------------------------------------------------------------- /examples/n-api/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /examples/n-api/README.md: -------------------------------------------------------------------------------- 1 | # Piscina N-API (native addon) example 2 | 3 | A Piscina worker can be implemented using a native addon, allowing 4 | work to performed by native code. This provides a number of interesting 5 | options including implementing workers using other languages such as 6 | Rust. 7 | 8 | To get started with this example, first install the dependencies: 9 | 10 | ```console 11 | $ npm i 12 | ``` 13 | 14 | Then build the artifacts: 15 | 16 | ```console 17 | $ npm run prebuild 18 | ``` 19 | 20 | The `prebuild` command will build the binary artifacts for the native 21 | addon and will put them in the `prebuilds` folder. Because of how 22 | prebuilds work, we need to use an intermediate JavaScript file to 23 | load and export them. For this example native addon, you'll find 24 | that in the `examples` folder. 25 | 26 | The index.js illustrates how to load and use the native addon as the 27 | worker implementation: 28 | 29 | ```js 30 | const Piscina = require('piscina'); 31 | const { resolve } = require('path'); 32 | 33 | const pool = new Piscina({ 34 | filename: resolve(__dirname, 'example') 35 | }); 36 | 37 | (async () => { 38 | // Submit 5 concurrent tasks 39 | console.log(await Promise.all([ 40 | pool.run(), 41 | pool.run(), 42 | pool.run(), 43 | pool.run(), 44 | pool.run() 45 | ])); 46 | })(); 47 | ``` 48 | -------------------------------------------------------------------------------- /examples/n-api/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name": "example", 4 | "include_dirs": [ " 2 | 3 | #include 4 | #include 5 | 6 | namespace example { 7 | 8 | using namespace Napi; 9 | 10 | // The Run function provides the implementation of the worker. 11 | // info argument will contain the input arguments. To perform 12 | // work asynchronously, this must return a Promise. 13 | static String Run(const CallbackInfo& info) { 14 | // Artificially block the thread, simulating block activity 15 | std::this_thread::sleep_for(std::chrono::seconds(1)); 16 | return String::New(info.Env(), "Hello World"); 17 | } 18 | 19 | Object Init(Env env, Object exports) { 20 | return Function::New(env, Run); 21 | } 22 | 23 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) 24 | 25 | } // namespace example 26 | -------------------------------------------------------------------------------- /examples/n-api/example/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | module.exports = require('node-gyp-build')(join(__dirname, '..')); 3 | -------------------------------------------------------------------------------- /examples/n-api/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /examples/n-api/index.js: -------------------------------------------------------------------------------- 1 | const Piscina = require('../..'); 2 | const { resolve } = require('path'); 3 | 4 | const pool = new Piscina({ 5 | filename: resolve(__dirname, 'example') 6 | }); 7 | 8 | (async () => { 9 | console.log(await Promise.all([ 10 | pool.run(), 11 | pool.run(), 12 | pool.run(), 13 | pool.run(), 14 | pool.run() 15 | ])); 16 | })(); 17 | -------------------------------------------------------------------------------- /examples/n-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piscina-napi-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "prebuildify --napi", 8 | "install": "node-gyp-build" 9 | }, 10 | "keywords": [], 11 | "author": "James M Snell ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "node-addon-api": "^3.0.0", 15 | "node-gyp-build": "^4.2.2", 16 | "prebuildify": "^4.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/named/helper.js: -------------------------------------------------------------------------------- 1 | function makeTask (op, ...args) { 2 | return { op, args }; 3 | } 4 | 5 | function dispatcher (obj) { 6 | return async ({ op, args }) => { 7 | return await obj[op](...args); 8 | }; 9 | } 10 | 11 | module.exports = { 12 | dispatcher, 13 | makeTask 14 | }; 15 | -------------------------------------------------------------------------------- /examples/named/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | const { makeTask } = require('./helper'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js') 9 | }); 10 | 11 | (async function () { 12 | const result = await Promise.all([ 13 | piscina.run(makeTask('add', 4, 6)), 14 | piscina.run(makeTask('sub', 4, 6)) 15 | ]); 16 | console.log(result); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/named/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { dispatcher } = require('./helper'); 4 | 5 | module.exports = dispatcher({ 6 | add (a, b) { return a + b; }, 7 | sub (a, b) { return a - b; } 8 | }); 9 | -------------------------------------------------------------------------------- /examples/progress/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | const ProgressBar = require('progress'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js') 9 | }); 10 | 11 | // Illustrates using a MessageChannel to allow the worker to 12 | // notify the main thread about current progress. 13 | 14 | async function task (a, b) { 15 | const bar = new ProgressBar(':bar [:current/:total]', { total: b }); 16 | const mc = new MessageChannel(); 17 | mc.port2.onmessage = () => bar.tick(); 18 | mc.port2.unref(); 19 | return await piscina.run({ a, b, port: mc.port1 }, { transferList: [mc.port1] }); 20 | } 21 | 22 | Promise.all([ 23 | task(0, 50), 24 | task(0, 25), 25 | task(0, 90) 26 | ]).catch((err) => console.error(err)); 27 | -------------------------------------------------------------------------------- /examples/progress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "progress", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "progress": "^2.0.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/progress/worker.js: -------------------------------------------------------------------------------- 1 | const { setTimeout } = require('timers/promises'); 2 | 3 | module.exports = async ({ a, b, port }) => { 4 | for (let n = a; n < b; n++) { 5 | await setTimeout(10); 6 | port.postMessage(n); 7 | } 8 | port.close(); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/react-ssr/components/greeting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | 5 | class Greeting extends React.Component { 6 | render () { 7 | return React.createElement('div', null, 'hello ' + this.props.name); 8 | } 9 | } 10 | 11 | module.exports = Greeting; 12 | -------------------------------------------------------------------------------- /examples/react-ssr/components/index.js: -------------------------------------------------------------------------------- 1 | const Greeting = require('./greeting'); 2 | const Lorem = require('./lorem'); 3 | 4 | module.exports = { 5 | Greeting, 6 | Lorem 7 | }; 8 | -------------------------------------------------------------------------------- /examples/react-ssr/components/lorem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | const { LoremIpsum } = require('lorem-ipsum'); 5 | 6 | class Paragraph extends React.Component { 7 | #lorem; 8 | 9 | constructor (props) { 10 | super(props); 11 | this.#lorem = new LoremIpsum({ 12 | sentencesPerParagraph: { 13 | max: 8, 14 | min: 4 15 | }, 16 | wordsPerSentence: { 17 | max: 16, 18 | min: 4 19 | } 20 | }); 21 | } 22 | 23 | render () { 24 | return React.createElement('div', null, this.#lorem.generateParagraphs(1)); 25 | } 26 | } 27 | 28 | class Lorem extends React.Component { 29 | render () { 30 | const children = []; 31 | for (let n = 0; n < Math.floor(Math.random() * 50); n++) { 32 | children.push(React.createElement(Paragraph, { key: n })); 33 | } 34 | return React.createElement('div', null, children); 35 | } 36 | } 37 | 38 | module.exports = Lorem; 39 | -------------------------------------------------------------------------------- /examples/react-ssr/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "components", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /examples/react-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ssr", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "fastify": "^2.14.1", 14 | "fastify-piscina": "^1.0.0", 15 | "lorem-ipsum": "^2.0.3", 16 | "notare": "^1.0.6", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-ssr/pooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fastify = require('fastify')(); 4 | const { resolve } = require('path'); 5 | 6 | fastify.register(require('fastify-piscina'), { 7 | filename: resolve(__dirname, 'worker.js'), 8 | execArgv: [], 9 | minThreads: 6, 10 | maxThreads: 6 11 | }); 12 | 13 | // Declare a route 14 | fastify.get('/', async () => fastify.run({ name: 'James' })); 15 | 16 | // Run the server! 17 | const start = async () => { 18 | try { 19 | await fastify.listen(3000); 20 | } catch (err) { 21 | process.exit(1); 22 | } 23 | }; 24 | start(); 25 | 26 | process.on('SIGINT', () => { 27 | const waitTime = fastify.piscina.waitTime; 28 | console.log('\nMax Queue Wait Time:', waitTime.max); 29 | console.log('Mean Queue Wait Time:', waitTime.mean); 30 | process.exit(0); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/react-ssr/unpooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fastify = require('fastify')(); 4 | 5 | const React = require('react'); 6 | const ReactDOMServer = require('react-dom/server'); 7 | const { Greeting, Lorem } = require('./components'); 8 | 9 | // Declare a route 10 | fastify.get('/', async () => { 11 | const name = 'James'; 12 | return ` 13 | 14 | 15 | 16 |
${ 17 | ReactDOMServer.renderToString(React.createElement(Greeting, { name })) 18 | }
19 | ${ 20 | ReactDOMServer.renderToString(React.createElement(Lorem)) 21 | } 22 | 23 | 24 | `; 25 | }); 26 | 27 | // Run the server! 28 | const start = async () => { 29 | try { 30 | await fastify.listen(3000); 31 | } catch (err) { 32 | process.exit(1); 33 | } 34 | }; 35 | start(); 36 | -------------------------------------------------------------------------------- /examples/react-ssr/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | const ReactDOMServer = require('react-dom/server'); 5 | 6 | const { Greeting, Lorem } = require('./components'); 7 | 8 | module.exports = ({ name }) => { 9 | return ` 10 | 11 | 12 | 13 |
${ 14 | ReactDOMServer.renderToString(React.createElement(Greeting, { name })) 15 | }
16 | ${ 17 | ReactDOMServer.renderToString(React.createElement(Lorem)) 18 | } 19 | 20 | 21 | `; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/resourceLimits/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | const { strictEqual } = require('assert'); 6 | 7 | const piscina = new Piscina({ 8 | filename: resolve(__dirname, 'worker.js'), 9 | resourceLimits: { 10 | maxOldGenerationSizeMb: 16, 11 | maxYoungGenerationSizeMb: 4, 12 | codeRangeSizeMb: 16 13 | } 14 | }); 15 | 16 | (async function () { 17 | try { 18 | await piscina.run(); 19 | } catch (err) { 20 | console.log('Worker terminated due to resource limits'); 21 | strictEqual(err.code, 'ERR_WORKER_OUT_OF_MEMORY'); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /examples/resourceLimits/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const array = []; 5 | while (true) { 6 | array.push([array]); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /examples/scrypt/README.md: -------------------------------------------------------------------------------- 1 | # scrypt Example 2 | 3 | This is an example comparing the performance of pooled and unpooled 4 | sync and async scrypt operations. 5 | 6 | ```console 7 | $ npm run pooled 100 8 | $ npm run unpooled 100 9 | $ npm run pooled-sync 100 10 | $ npm run unpooled-sync 100 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/scrypt/monitor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { monitorEventLoopDelay } = require('perf_hooks'); 4 | const { isMainThread } = require('worker_threads'); 5 | 6 | if (isMainThread) { 7 | const monitor = monitorEventLoopDelay({ resolution: 20 }); 8 | 9 | monitor.enable(); 10 | 11 | process.on('exit', () => { 12 | monitor.disable(); 13 | console.log( 14 | 'Main Thread Mean/Max/99% Event Loop Delay:', 15 | monitor.mean, 16 | monitor.max, 17 | monitor.percentile(99) 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /examples/scrypt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrypt", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "pooled": "node -r ./monitor pooled", 6 | "unpooled": "node -r ./monitor unpooled", 7 | "pooled-sync": "node -r ./monitor pooled_sync", 8 | "unpooled-sync": "node -r ./monitor unpooled_sync" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "description": "" 14 | } 15 | -------------------------------------------------------------------------------- /examples/scrypt/pooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | const crypto = require('crypto'); 6 | const { promisify } = require('util'); 7 | const randomFill = promisify(crypto.randomFill); 8 | const { performance, PerformanceObserver } = require('perf_hooks'); 9 | 10 | const obs = new PerformanceObserver((entries) => { 11 | console.log(entries.getEntries()[0].duration); 12 | }); 13 | obs.observe({ entryTypes: ['measure'] }); 14 | 15 | const piscina = new Piscina({ 16 | filename: resolve(__dirname, 'scrypt.js'), 17 | concurrentTasksPerWorker: 10 18 | }); 19 | 20 | process.on('exit', () => { 21 | const { runTime, waitTime } = piscina; 22 | console.log('Run Time Average:', runTime.average); 23 | console.log('Run Time Mean/Stddev:', runTime.mean, runTime.stddev); 24 | console.log('Run Time Min:', runTime.min); 25 | console.log('Run Time Max:', runTime.max); 26 | console.log('Wait Time Average:', waitTime.average); 27 | console.log('Wait Time Mean/Stddev:', waitTime.mean, waitTime.stddev); 28 | console.log('Wait Time Min:', waitTime.min); 29 | console.log('Wait Time Max:', waitTime.max); 30 | }); 31 | 32 | async function * generateInput () { 33 | let max = parseInt(process.argv[2] || 10); 34 | const data = Buffer.allocUnsafe(10); 35 | while (max-- > 0) { 36 | yield randomFill(data); 37 | } 38 | } 39 | 40 | (async function () { 41 | performance.mark('start'); 42 | const keylen = 64; 43 | 44 | for await (const input of generateInput()) { 45 | await piscina.run({ input, keylen }); 46 | } 47 | 48 | performance.mark('end'); 49 | performance.measure('start to end', 'start', 'end'); 50 | })(); 51 | -------------------------------------------------------------------------------- /examples/scrypt/pooled_sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | const crypto = require('crypto'); 6 | const { promisify } = require('util'); 7 | const randomFill = promisify(crypto.randomFill); 8 | const { performance, PerformanceObserver } = require('perf_hooks'); 9 | 10 | const obs = new PerformanceObserver((entries) => { 11 | console.log(entries.getEntries()[0].duration); 12 | }); 13 | obs.observe({ entryTypes: ['measure'] }); 14 | 15 | const piscina = new Piscina({ 16 | filename: resolve(__dirname, 'scrypt_sync.js') 17 | }); 18 | 19 | process.on('exit', () => { 20 | const { runTime, waitTime } = piscina; 21 | console.log('Run Time Average:', runTime.average); 22 | console.log('Run Time Mean/Stddev:', runTime.mean, runTime.stddev); 23 | console.log('Run Time Min:', runTime.min); 24 | console.log('Run Time Max:', runTime.max); 25 | console.log('Wait Time Average:', waitTime.average); 26 | console.log('Wait Time Mean/Stddev:', waitTime.mean, waitTime.stddev); 27 | console.log('Wait Time Min:', waitTime.min); 28 | console.log('Wait Time Max:', waitTime.max); 29 | }); 30 | 31 | async function * generateInput () { 32 | let max = parseInt(process.argv[2] || 10); 33 | const data = Buffer.allocUnsafe(10); 34 | while (max-- > 0) { 35 | yield randomFill(data); 36 | } 37 | } 38 | 39 | (async function () { 40 | performance.mark('start'); 41 | const keylen = 64; 42 | 43 | for await (const input of generateInput()) { 44 | await piscina.run({ input, keylen }); 45 | } 46 | 47 | performance.mark('end'); 48 | performance.measure('start to end', 'start', 'end'); 49 | })(); 50 | -------------------------------------------------------------------------------- /examples/scrypt/scrypt.js: -------------------------------------------------------------------------------- 1 | // eslint-disable no-unused-vars 2 | 'use strict'; 3 | 4 | const crypto = require('crypto'); 5 | const { promisify } = require('util'); 6 | const scrypt = promisify(crypto.scrypt); 7 | const randomFill = promisify(crypto.randomFill); 8 | 9 | const salt = Buffer.allocUnsafe(16); 10 | 11 | module.exports = async function ({ 12 | input, 13 | keylen, 14 | N = 16384, 15 | r = 8, 16 | p = 1, 17 | maxmem = 32 * 1024 * 1024 18 | }) { 19 | return (await scrypt( 20 | input, 21 | await randomFill(salt), 22 | keylen, { N, r, p, maxmem })).toString('hex'); 23 | }; 24 | -------------------------------------------------------------------------------- /examples/scrypt/scrypt_sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { scryptSync, randomFillSync } = require('crypto'); 4 | 5 | const salt = Buffer.allocUnsafe(16); 6 | 7 | module.exports = function ({ 8 | input, 9 | keylen, 10 | N = 16384, 11 | r = 8, 12 | p = 1, 13 | maxmem = 32 * 1024 * 1024 14 | }) { 15 | return scryptSync(input, 16 | randomFillSync(salt), 17 | keylen, 18 | { N, r, p, maxmem }).toString('hex'); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/scrypt/unpooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | const { promisify } = require('util'); 5 | const randomFill = promisify(crypto.randomFill); 6 | const scrypt = promisify(crypto.scrypt); 7 | const { performance, PerformanceObserver } = require('perf_hooks'); 8 | 9 | const salt = Buffer.allocUnsafe(16); 10 | 11 | const obs = new PerformanceObserver((entries) => { 12 | console.log(entries.getEntries()[0].duration); 13 | }); 14 | obs.observe({ entryTypes: ['measure'] }); 15 | 16 | async function * generateInput () { 17 | let max = parseInt(process.argv[2] || 10); 18 | const data = Buffer.allocUnsafe(10); 19 | while (max-- > 0) { 20 | yield randomFill(data); 21 | } 22 | } 23 | 24 | (async function () { 25 | performance.mark('start'); 26 | const keylen = 64; 27 | 28 | for await (const input of generateInput()) { 29 | (await scrypt(input, await randomFill(salt), keylen)).toString('hex'); 30 | } 31 | performance.mark('end'); 32 | performance.measure('start to end', 'start', 'end'); 33 | })(); 34 | -------------------------------------------------------------------------------- /examples/scrypt/unpooled_sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | const { promisify } = require('util'); 5 | const { scryptSync, randomFillSync } = crypto; 6 | const randomFill = promisify(crypto.randomFill); 7 | const { performance, PerformanceObserver } = require('perf_hooks'); 8 | 9 | const salt = Buffer.allocUnsafe(16); 10 | 11 | const obs = new PerformanceObserver((entries) => { 12 | console.log(entries.getEntries()[0].duration); 13 | }); 14 | obs.observe({ entryTypes: ['measure'] }); 15 | 16 | async function * generateInput () { 17 | let max = parseInt(process.argv[2] || 10); 18 | const data = Buffer.allocUnsafe(10); 19 | while (max-- > 0) { 20 | yield randomFill(data); 21 | } 22 | } 23 | 24 | (async function () { 25 | performance.mark('start'); 26 | const keylen = 64; 27 | 28 | for await (const input of generateInput()) { 29 | // Everything in here is intentionally sync 30 | scryptSync(input, randomFillSync(salt), keylen).toString('hex'); 31 | } 32 | performance.mark('end'); 33 | performance.measure('start to end', 'start', 'end'); 34 | })(); 35 | -------------------------------------------------------------------------------- /examples/server/async-sleep-pooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fastify = require('fastify')(); 4 | const { resolve } = require('path'); 5 | 6 | const concurrentTasksPerWorker = parseInt(process.argv[2] || 1); 7 | const idleTimeout = parseInt(process.argv[3] || 0); 8 | 9 | fastify.register(require('fastify-piscina'), { 10 | filename: resolve(__dirname, 'worker.js'), 11 | concurrentTasksPerWorker, 12 | idleTimeout 13 | }); 14 | 15 | // Declare a route 16 | fastify.get('/', () => fastify.run()); 17 | 18 | // Run the server! 19 | const start = async () => { 20 | try { 21 | await fastify.listen(3000); 22 | } catch (err) { 23 | process.exit(1); 24 | } 25 | }; 26 | start(); 27 | -------------------------------------------------------------------------------- /examples/server/async-sleep-unpooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fastify = require('fastify')(); 4 | const { promisify } = require('util'); 5 | const sleep = promisify(setTimeout); 6 | 7 | // Declare a route 8 | fastify.get('/', async () => { 9 | await sleep(100); 10 | return { hello: 'world' }; 11 | }); 12 | 13 | // Run the server! 14 | const start = async () => { 15 | try { 16 | await fastify.listen(3000); 17 | } catch (err) { 18 | process.exit(1); 19 | } 20 | }; 21 | start(); 22 | -------------------------------------------------------------------------------- /examples/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tmp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "fastify": "^2.14.0", 14 | "fastify-piscina": "^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/server/sync-sleep-pooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fastify = require('fastify')(); 4 | const { resolve } = require('path'); 5 | 6 | const concurrentTasksPerWorker = parseInt(process.argv[2] || 1); 7 | const idleTimeout = parseInt(process.argv[3] || 0); 8 | 9 | fastify.register(require('fastify-piscina'), { 10 | filename: resolve(__dirname, 'worker2.js'), 11 | concurrentTasksPerWorker, 12 | idleTimeout 13 | }); 14 | 15 | // Declare a route 16 | fastify.get('/', () => fastify.run()); 17 | 18 | // Run the server! 19 | const start = async () => { 20 | try { 21 | await fastify.listen(3000); 22 | } catch (err) { 23 | process.exit(1); 24 | } 25 | }; 26 | start(); 27 | -------------------------------------------------------------------------------- /examples/server/sync-sleep-unpooled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fastify = require('fastify')(); 4 | 5 | const sab = new SharedArrayBuffer(4); 6 | const lock = new Int32Array(sab); 7 | 8 | // Declare a route 9 | fastify.get('/', async () => { 10 | Atomics.wait(lock, 0, 0, 100); 11 | return { hello: 'world' }; 12 | }); 13 | 14 | // Run the server! 15 | const start = async () => { 16 | try { 17 | await fastify.listen(3000); 18 | } catch (err) { 19 | process.exit(1); 20 | } 21 | }; 22 | start(); 23 | -------------------------------------------------------------------------------- /examples/server/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | module.exports = async () => { 7 | await sleep(100); 8 | return { hello: 'world' }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/server/worker2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sab = new SharedArrayBuffer(4); 4 | const lock = new Int32Array(sab); 5 | 6 | module.exports = async () => { 7 | Atomics.wait(lock, 0, 0, 100); 8 | return { hello: 'world' }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | 6 | const piscina = new Piscina({ 7 | filename: resolve(__dirname, 'worker.js') 8 | }); 9 | 10 | (async function () { 11 | const result = await piscina.run({ a: 4, b: 6 }); 12 | console.log(result); // Prints 10 13 | })(); 14 | -------------------------------------------------------------------------------- /examples/simple/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ a, b }) => { 4 | return a + b; 5 | }; 6 | -------------------------------------------------------------------------------- /examples/simple_async/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | 6 | const piscina = new Piscina({ 7 | filename: resolve(__dirname, 'worker.js') 8 | }); 9 | 10 | (async function () { 11 | const result = await piscina.run({ a: 4, b: 6 }); 12 | console.log(result); // Prints 10 13 | })(); 14 | -------------------------------------------------------------------------------- /examples/simple_async/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | module.exports = async ({ a, b }) => { 7 | // Fake some async activity 8 | await sleep(100); 9 | return a + b; 10 | }; 11 | -------------------------------------------------------------------------------- /examples/stream-in/.gitignore: -------------------------------------------------------------------------------- 1 | data.csv 2 | -------------------------------------------------------------------------------- /examples/stream-in/generate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createWriteStream } = require('fs'); 4 | 5 | const out = createWriteStream('./data.csv'); 6 | 7 | const count = parseInt(process.argv[2] || '5000') || 5000; 8 | 9 | out.write('a,b,c,d,e,f,g\n'); 10 | 11 | for (let n = 0; n < count; n++) { 12 | out.write('1,2,3,4,5,6,7\n'); 13 | } 14 | 15 | out.end(); 16 | console.log('done'); 17 | -------------------------------------------------------------------------------- /examples/stream-in/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // node index [maxQueue] 4 | // example: node index 5 | // defaults to 100 6 | // example: node index 100 7 | // example: node index 500 8 | 9 | const { resolve } = require('path'); 10 | const csv = require('csvtojson'); 11 | const Pool = require('../..'); 12 | const { performance, PerformanceObserver } = require('perf_hooks'); 13 | const Progress = require('./progress'); 14 | 15 | const p = new PerformanceObserver((entries) => { 16 | console.log(entries.getEntries()[0].duration); 17 | }); 18 | p.observe({ entryTypes: ['measure'] }); 19 | 20 | const maxQueue = Math.max(parseInt(process.argv[2] || 100), 50); 21 | 22 | const pool = new Pool({ 23 | filename: resolve(__dirname, 'worker.js'), 24 | maxQueue 25 | }); 26 | 27 | const stream = csv().fromFile('./data.csv'); 28 | 29 | pool.on('drain', () => { 30 | if (stream.isPaused()) { 31 | console.log('resuming...', pool.queueSize); 32 | stream.resume(); 33 | } 34 | }); 35 | 36 | const progress = new Progress(); 37 | progress.on('finished', () => { 38 | console.log(progress.message); 39 | }); 40 | 41 | performance.mark('A'); 42 | stream 43 | .on('data', (data) => { 44 | const line = data.toString('utf8'); 45 | progress.incSubmitted(); 46 | pool.run(line) 47 | .then(() => { 48 | progress.incCompleted(); 49 | }) 50 | .catch((err) => { 51 | progress.incFailed(); 52 | stream.destroy(err); 53 | }); 54 | if (pool.queueSize === maxQueue) { 55 | console.log('pausing...', pool.queueSize, pool.utilization); 56 | stream.pause(); 57 | } 58 | }) 59 | .on('error', (err) => { 60 | console.log(err.message); 61 | console.log('run: `node generate` to generate the sample data'); 62 | }) 63 | .on('end', () => { 64 | // We are done submitting tasks 65 | progress.done(); 66 | performance.mark('B'); 67 | performance.measure('A to B', 'A', 'B'); 68 | }); 69 | 70 | process.on('exit', () => { 71 | console.log('Mean Wait Time:', pool.waitTime.mean, 'ms'); 72 | console.log('Mean Run Time:', pool.runTime.mean, 'ms'); 73 | }); 74 | -------------------------------------------------------------------------------- /examples/stream-in/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-in", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "csvtojson": "^2.0.10" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/stream-in/progress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { EventEmitter } = require('events'); 3 | 4 | class Progress extends EventEmitter { 5 | #tasksSubmitted = 0; 6 | #tasksCompleted = 0; 7 | #tasksFailed = 0; 8 | #done = false; 9 | 10 | done () { 11 | this.#done = true; 12 | } 13 | 14 | incSubmitted () { 15 | this.#tasksSubmitted++; 16 | } 17 | 18 | incCompleted () { 19 | this.#tasksCompleted++; 20 | if (this.#done && this.completed === this.#tasksSubmitted) { 21 | process.nextTick(() => this.emit('finished')); 22 | } 23 | } 24 | 25 | incFailed () { 26 | this.#tasksFailed++; 27 | } 28 | 29 | get completed () { 30 | return this.#tasksCompleted + this.#tasksFailed; 31 | } 32 | 33 | get message () { 34 | return `${this.#tasksCompleted} of ${this.#tasksSubmitted} completed` + 35 | ` ${((this.#tasksCompleted / this.#tasksSubmitted) * 100).toFixed(2)}%` + 36 | ` [${this.#tasksFailed} failed]`; 37 | } 38 | } 39 | 40 | module.exports = Progress; 41 | -------------------------------------------------------------------------------- /examples/stream-in/worker.js: -------------------------------------------------------------------------------- 1 | module.exports = (data) => { 2 | return JSON.stringify(data); 3 | }; 4 | -------------------------------------------------------------------------------- /examples/stream/index.mjs: -------------------------------------------------------------------------------- 1 | import Piscina from 'piscina'; 2 | import { MessagePortDuplex } from './stream.mjs'; 3 | import { createReadStream } from 'fs'; 4 | import { pipeline } from 'stream'; 5 | 6 | const pool = new Piscina({ 7 | filename: new URL('./worker.mjs', import.meta.url).href 8 | }); 9 | 10 | const { port1, port2 } = new MessageChannel(); 11 | 12 | pool.run(port2, { transferList: [port2] }); 13 | 14 | const duplex = new MessagePortDuplex(port1); 15 | pipeline( 16 | createReadStream(new URL('./index.mjs', import.meta.url).pathname), 17 | duplex, 18 | process.stdout, 19 | (err) => { if (err) throw err; }); 20 | -------------------------------------------------------------------------------- /examples/stream/stream.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | Writable, 3 | Readable, 4 | Duplex 5 | } from 'stream'; 6 | 7 | const kPort = Symbol('kPort'); 8 | 9 | export class MessagePortWritable extends Writable { 10 | constructor (port, options) { 11 | super(options); 12 | this[kPort] = port; 13 | } 14 | 15 | _write (buf, _, cb) { 16 | this[kPort].postMessage(buf, [buf.buffer]); 17 | cb(); 18 | } 19 | 20 | _writev (data, cb) { 21 | const chunks = new Array(data.length); 22 | const transfers = new Array(data.length); 23 | for (let n = 0; n < data.length; n++) { 24 | chunks[n] = data[n].chunk; 25 | transfers[n] = data[n].chunk.buffs; 26 | } 27 | this[kPort].postMessage(chunks, transfers); 28 | cb(); 29 | } 30 | 31 | _final (cb) { 32 | this[kPort].postMessage(null); 33 | cb(); 34 | } 35 | 36 | _destroy (err, cb) { 37 | this[kPort].close(() => cb(err)); 38 | } 39 | 40 | unref () { this[kPort].unref(); return this; } 41 | ref () { this[kPort].ref(); return this; } 42 | } 43 | 44 | export class MessagePortReadable extends Readable { 45 | constructor (port, options) { 46 | super(options); 47 | this[kPort] = port; 48 | port.onmessage = ({ data }) => this.push(data); 49 | } 50 | 51 | _read () { 52 | this[kPort].start(); 53 | } 54 | 55 | _destroy (err, cb) { 56 | this[kPort].close(() => { 57 | this[kPort].onmessage = undefined; 58 | cb(err); 59 | }); 60 | } 61 | 62 | unref () { this[kPort].unref(); return this; } 63 | ref () { this[kPort].ref(); return this; } 64 | } 65 | 66 | export class MessagePortDuplex extends Duplex { 67 | constructor (port, options) { 68 | super(options); 69 | this[kPort] = port; 70 | port.onmessage = ({ data }) => this.push(data); 71 | } 72 | 73 | _read () { 74 | this[kPort].start(); 75 | } 76 | 77 | _write (buf, _, cb) { 78 | this[kPort].postMessage(buf, [buf.buffer]); 79 | cb(); 80 | } 81 | 82 | _writev (data, cb) { 83 | const chunks = new Array(data.length); 84 | const transfers = new Array(data.length); 85 | for (let n = 0; n < data.length; n++) { 86 | chunks[n] = data[n].chunk; 87 | transfers[n] = data[n].chunk.buffs; 88 | } 89 | this[kPort].postMessage(chunks, transfers); 90 | cb(); 91 | } 92 | 93 | _final (cb) { 94 | this[kPort].postMessage(null); 95 | cb(); 96 | } 97 | 98 | _destroy (err, cb) { 99 | this[kPort].close(() => { 100 | this[kPort].onmessage = undefined; 101 | cb(err); 102 | }); 103 | } 104 | 105 | unref () { this[kPort].unref(); return this; } 106 | ref () { this[kPort].ref(); return this; } 107 | } 108 | -------------------------------------------------------------------------------- /examples/stream/worker.mjs: -------------------------------------------------------------------------------- 1 | import { MessagePortDuplex } from './stream.mjs'; 2 | 3 | export default function (port) { 4 | let res; 5 | const ret = new Promise((resolve) => { 6 | res = resolve; 7 | }); 8 | const duplex = new MessagePortDuplex(port); 9 | duplex.setEncoding('utf8'); 10 | duplex.on('data', (chunk) => duplex.write(chunk.toUpperCase())); 11 | duplex.on('end', () => { 12 | duplex.end(); 13 | res(); 14 | }); 15 | return ret; 16 | } 17 | -------------------------------------------------------------------------------- /examples/task-queue/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const spq = require('shuffled-priority-queue'); 4 | const Piscina = require('../..'); 5 | const { resolve } = require('path'); 6 | 7 | // By default, Piscina uses a simple Fifo array task queue. 8 | // This example replaces the default task queue with a 9 | // priority queue. 10 | 11 | // When a task is submitted to the pool, if there are workers 12 | // available it will be dispatched immediately, regardless of 13 | // the priority. The task queue is only used if there are no 14 | // free workers 15 | 16 | const kItem = Symbol('item'); 17 | 18 | class PriorityTaskQueue { 19 | queue = spq(); 20 | 21 | get size () { return this.queue.length; } 22 | 23 | push (value) { 24 | const queueOptions = value[Piscina.queueOptionsSymbol]; 25 | const priority = queueOptions ? (queueOptions.priority || 0) : 0; 26 | value[kItem] = this.queue.add({ priority, value }); 27 | } 28 | 29 | remove (value) { 30 | this.queue.remove(value[kItem]); 31 | } 32 | 33 | shift () { 34 | return this.queue.shift().value; 35 | } 36 | } 37 | 38 | const pool = new Piscina({ 39 | filename: resolve(__dirname, 'worker.js'), 40 | taskQueue: new PriorityTaskQueue(), 41 | maxThreads: 4 42 | }); 43 | 44 | function makeTask (task, priority) { 45 | return { ...task, [Piscina.queueOptionsSymbol]: { priority } }; 46 | } 47 | 48 | (async () => { 49 | // Submit enough tasks to ensure that at least some are queued 50 | console.log(await Promise.all([ 51 | pool.run(makeTask({ priority: 1 }, 1)), 52 | pool.run(makeTask({ priority: 2 }, 2)), 53 | pool.run(makeTask({ priority: 3 }, 3)), 54 | pool.run(makeTask({ priority: 4 }, 4)), 55 | pool.run(makeTask({ priority: 5 }, 5)), 56 | pool.run(makeTask({ priority: 6 }, 6)), 57 | pool.run(makeTask({ priority: 7 }, 7)), 58 | pool.run(makeTask({ priority: 8 }, 8)) 59 | ])); 60 | })(); 61 | -------------------------------------------------------------------------------- /examples/task-queue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-queue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "shuffled-priority-queue": "^2.1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/task-queue/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | module.exports = async ({ priority }) => { 7 | await sleep(100); 8 | process._rawDebug(`${priority}`); 9 | return priority; 10 | }; 11 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "start": "node lib/index", 8 | "build": "tsc" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "typescript": "^4.1.2" 14 | }, 15 | "typings": "./lib/index.d.ts" 16 | } 17 | -------------------------------------------------------------------------------- /examples/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '../../..'; 2 | import { isMainThread } from 'worker_threads'; 3 | 4 | interface Inputs { 5 | a: number; 6 | b: number; 7 | } 8 | 9 | if (isMainThread) { 10 | const piscina = new Piscina({ filename: __filename }); 11 | 12 | (async function () { 13 | const task: Inputs = { a: 1, b: 2 }; 14 | console.log(await piscina.run(task)); 15 | })(); 16 | } else { 17 | module.exports = ({ a, b }: Inputs): number => { 18 | return a + b; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "lib": ["es2018"], 7 | "rootDir": "src", 8 | "outDir": "lib", 9 | "strict": true, 10 | "alwaysStrict": true, 11 | "strictFunctionTypes": true, 12 | "strictNullChecks": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "downlevelIteration": true, 21 | "declaration": true, 22 | "esModuleInterop": true, 23 | "allowSyntheticDefaultImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/webstreams-transform/index.mjs: -------------------------------------------------------------------------------- 1 | import Piscina from 'piscina'; 2 | import { 3 | ReadableStream, 4 | TransformStream, 5 | WritableStream 6 | } from 'node:stream/web'; 7 | 8 | const pool = new Piscina({ 9 | filename: new URL('./worker.mjs', import.meta.url).href 10 | }); 11 | 12 | const readable = new ReadableStream({ 13 | start () { 14 | this.chunks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; 15 | }, 16 | 17 | pull (controller) { 18 | const chunk = this.chunks.shift(); 19 | controller.enqueue(chunk); 20 | if (this.chunks.length === 0) { 21 | controller.close(); 22 | } 23 | } 24 | }); 25 | 26 | const writable = new WritableStream({ 27 | write (chunk) { 28 | console.log(chunk); 29 | } 30 | }); 31 | 32 | const transform = new TransformStream({ 33 | async transform (chunk, controller) { 34 | controller.enqueue(await pool.run(chunk)); 35 | } 36 | }); 37 | 38 | readable.pipeThrough(transform).pipeTo(writable); 39 | -------------------------------------------------------------------------------- /examples/webstreams-transform/worker.mjs: -------------------------------------------------------------------------------- 1 | export default async function (num) { 2 | return 'ABC'.repeat(num * num); 3 | } 4 | -------------------------------------------------------------------------------- /examples/webstreams/index.mjs: -------------------------------------------------------------------------------- 1 | import Piscina from 'piscina'; 2 | import { 3 | ReadableStream, 4 | WritableStream 5 | } from 'node:stream/web'; 6 | 7 | const pool = new Piscina({ 8 | filename: new URL('./worker.mjs', import.meta.url).href 9 | }); 10 | 11 | const readable = new ReadableStream({ 12 | start () { 13 | this.chunks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; 14 | }, 15 | 16 | pull (controller) { 17 | const chunk = this.chunks.shift(); 18 | controller.enqueue(chunk); 19 | if (this.chunks.length === 0) { 20 | controller.close(); 21 | } 22 | } 23 | }); 24 | 25 | const writable = new WritableStream({ 26 | write (chunk) { 27 | console.log(chunk); 28 | } 29 | }); 30 | 31 | await pool.run({ readable, writable }, { transferList: [readable, writable] }); 32 | -------------------------------------------------------------------------------- /examples/webstreams/worker.mjs: -------------------------------------------------------------------------------- 1 | export default async function ({ readable, writable }) { 2 | const writer = writable.getWriter(); 3 | for await (const chunk of readable) { 4 | await writer.write(chunk); 5 | } 6 | writer.close(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/worker_options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { resolve } = require('path'); 5 | 6 | const piscina = new Piscina({ 7 | filename: resolve(__dirname, 'worker.js'), 8 | env: { A: '123' }, 9 | argv: ['a', 'b', 'c'], 10 | execArgv: ['--no-warnings'], 11 | workerData: 'ABC' 12 | }); 13 | 14 | (async function () { 15 | const result = await piscina.run({ a: 4, b: 6 }); 16 | console.log(result); // Prints 10 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/worker_options/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../..'); 4 | const { format } = require('util'); 5 | 6 | module.exports = ({ a, b }) => { 7 | console.log(` 8 | process.argv: ${process.argv.slice(2)} 9 | process.execArgv: ${process.execArgv} 10 | process.env: ${format({ ...process.env })} 11 | workerData: ${Piscina.workerData}`); 12 | return a + b; 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piscina", 3 | "version": "5.0.0", 4 | "description": "A fast, efficient Node.js Worker Thread Pool implementation", 5 | "main": "./dist/main.js", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | "types": "./dist/index.d.ts", 9 | "import": "./dist/esm-wrapper.mjs", 10 | "require": "./dist/main.js" 11 | }, 12 | "engines": { 13 | "node": ">=20.x" 14 | }, 15 | "scripts": { 16 | "build": "tsc && gen-esm-wrapper . dist/esm-wrapper.mjs", 17 | "lint": "eslint", 18 | "test": "c8 tap", 19 | "test:ci": "npm run lint && npm run build && npm run test:coverage", 20 | "test:coverage": "c8 --reporter=lcov tap --cov", 21 | "prepack": "npm run build", 22 | "benchmark": "npm run benchmark:queue && npm run benchmark:piscina", 23 | "benchmark:piscina": "npm run benchmark:default && npm run benchmark:queue:fixed && npm run benchmark:default:comparison", 24 | "benchmark:default": "node benchmark/simple-benchmark.js", 25 | "benchmark:default:async": "node benchmark/simple-benchmark.js", 26 | "benchmark:default:comparison": "node benchmark/piscina-queue-comparison.js", 27 | "benchmark:queue": "npm run benchmark:queue:comparison", 28 | "benchmark:queue:fixed": "node benchmark/simple-benchmark-fixed-queue.js", 29 | "benchmark:queue:comparison": "node benchmark/queue-comparison.js" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/piscinajs/piscina.git" 34 | }, 35 | "keywords": [ 36 | "fast", 37 | "worker threads", 38 | "thread pool", 39 | "wade wilson" 40 | ], 41 | "author": "James M Snell ", 42 | "contributors": [ 43 | "Anna Henningsen ", 44 | "Matteo Collina ", 45 | "Carlos Fuentes " 46 | ], 47 | "license": "MIT", 48 | "devDependencies": { 49 | "@types/node": "^22.4.1", 50 | "abort-controller": "^3.0.0", 51 | "c8": "^10.1.2", 52 | "concat-stream": "^2.0.0", 53 | "eslint": "^9.16.0", 54 | "gen-esm-wrapper": "^1.1.1", 55 | "neostandard": "^0.12.0", 56 | "tap": "^16.3.7", 57 | "tinybench": "^4.0.1", 58 | "ts-node": "^10.9.2", 59 | "typescript": "5.8.3" 60 | }, 61 | "optionalDependencies": { 62 | "@napi-rs/nice": "^1.0.1" 63 | }, 64 | "bugs": { 65 | "url": "https://github.com/piscinajs/piscina/issues" 66 | }, 67 | "homepage": "https://github.com/piscinajs/piscina#readme", 68 | "directories": { 69 | "example": "examples", 70 | "test": "test" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/abort.ts: -------------------------------------------------------------------------------- 1 | interface AbortSignalEventTargetAddOptions { 2 | once: boolean; 3 | } 4 | 5 | export interface AbortSignalEventTarget { 6 | addEventListener: ( 7 | name: 'abort', 8 | listener: () => void, 9 | options?: AbortSignalEventTargetAddOptions 10 | ) => void; 11 | removeEventListener: (name: 'abort', listener: () => void) => void; 12 | aborted?: boolean; 13 | reason?: unknown; 14 | } 15 | 16 | export interface AbortSignalEventEmitter { 17 | off: (name: 'abort', listener: () => void) => void; 18 | once: (name: 'abort', listener: () => void) => void; 19 | } 20 | 21 | export type AbortSignalAny = AbortSignalEventTarget | AbortSignalEventEmitter; 22 | 23 | export class AbortError extends Error { 24 | constructor (reason?: AbortSignalEventTarget['reason']) { 25 | // TS does not recognizes the cause clause 26 | // @ts-expect-error 27 | super('The task has been aborted', { cause: reason }); 28 | } 29 | 30 | get name () { 31 | return 'AbortError'; 32 | } 33 | } 34 | 35 | export function onabort (abortSignal: AbortSignalAny, listener: () => void) { 36 | if ('addEventListener' in abortSignal) { 37 | abortSignal.addEventListener('abort', listener, { once: true }); 38 | } else { 39 | abortSignal.once('abort', listener); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | import { availableParallelism } from 'node:os'; 3 | 4 | import { kMovable, kTransferable, kValue } from './symbols'; 5 | 6 | // States wether the worker is ready to receive tasks 7 | export const READY = '_WORKER_READY'; 8 | 9 | /** 10 | * True if the object implements the Transferable interface 11 | * 12 | * @export 13 | * @param {unknown} value 14 | * @return {*} {boolean} 15 | */ 16 | export function isTransferable (value: unknown): boolean { 17 | return ( 18 | value != null && 19 | typeof value === 'object' && 20 | kTransferable in value && 21 | kValue in value 22 | ); 23 | } 24 | 25 | /** 26 | * True if object implements Transferable and has been returned 27 | * by the Piscina.move() function 28 | * 29 | * TODO: narrow down the type of value 30 | * @export 31 | * @param {(unknown & PiscinaMovable)} value 32 | * @return {*} {boolean} 33 | */ 34 | export function isMovable (value: any): boolean { 35 | return isTransferable(value) && value[kMovable] === true; 36 | } 37 | 38 | export function markMovable (value: {}): void { 39 | Object.defineProperty(value, kMovable, { 40 | enumerable: false, 41 | configurable: true, 42 | writable: true, 43 | value: true 44 | }); 45 | } 46 | 47 | // State of Piscina pool 48 | export const commonState = { 49 | isWorkerThread: false, 50 | workerData: undefined 51 | }; 52 | 53 | export function maybeFileURLToPath (filename : string) : string { 54 | return filename.startsWith('file:') 55 | ? fileURLToPath(new URL(filename)) 56 | : filename; 57 | } 58 | 59 | export function getAvailableParallelism () : number { 60 | return availableParallelism(); 61 | } 62 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export const Errors = { 2 | ThreadTermination: () => new Error('Terminating worker thread'), 3 | FilenameNotProvided: () => 4 | new Error('filename must be provided to run() or in options object'), 5 | TaskQueueAtLimit: () => new Error('Task queue is at limit'), 6 | NoTaskQueueAvailable: () => 7 | new Error('No task queue available and all Workers are busy'), 8 | CloseTimeout: () => new Error('Close operation timed out') 9 | }; 10 | -------------------------------------------------------------------------------- /src/histogram.ts: -------------------------------------------------------------------------------- 1 | import { RecordableHistogram, createHistogram } from 'node:perf_hooks'; 2 | 3 | export type PiscinaHistogramSummary = { 4 | average: number; 5 | mean: number; 6 | stddev: number; 7 | min: number; 8 | max: number; 9 | p0_001: number; 10 | p0_01: number; 11 | p0_1: number; 12 | p1: number; 13 | p2_5: number; 14 | p10: number; 15 | p25: number; 16 | p50: number; 17 | p75: number; 18 | p90: number; 19 | p97_5: number; 20 | p99: number; 21 | p99_9: number; 22 | p99_99: number; 23 | p99_999: number; 24 | }; 25 | 26 | export type PiscinaHistogram = { 27 | runTime: PiscinaHistogramSummary; 28 | waitTime: PiscinaHistogramSummary; 29 | resetRunTime(): void; 30 | resetWaitTime(): void; 31 | }; 32 | 33 | export class PiscinaHistogramHandler { 34 | #runTime: RecordableHistogram; 35 | #waitTime: RecordableHistogram; 36 | 37 | constructor() { 38 | this.#runTime = createHistogram(); 39 | this.#waitTime = createHistogram(); 40 | } 41 | 42 | get runTimeSummary(): PiscinaHistogramSummary { 43 | return PiscinaHistogramHandler.createHistogramSummary(this.#runTime); 44 | } 45 | 46 | get waitTimeSummary(): PiscinaHistogramSummary { 47 | return PiscinaHistogramHandler.createHistogramSummary(this.#waitTime); 48 | } 49 | 50 | get runTimeCount(): number { 51 | return this.#runTime.count; 52 | } 53 | 54 | get waitTimeCount(): number { 55 | return this.#waitTime.count; 56 | } 57 | 58 | recordRunTime(value: number) { 59 | this.#runTime.record(PiscinaHistogramHandler.toHistogramIntegerNano(value)); 60 | } 61 | 62 | recordWaitTime(value: number) { 63 | this.#waitTime.record( 64 | PiscinaHistogramHandler.toHistogramIntegerNano(value) 65 | ); 66 | } 67 | 68 | resetWaitTime(): void { 69 | this.#waitTime.reset(); 70 | } 71 | 72 | resetRunTime(): void { 73 | this.#runTime.reset(); 74 | } 75 | 76 | static createHistogramSummary( 77 | histogram: RecordableHistogram 78 | ): PiscinaHistogramSummary { 79 | const { mean, stddev, min, max } = histogram; 80 | 81 | return { 82 | average: mean / 1000, 83 | mean: mean / 1000, 84 | stddev, 85 | min: min / 1000, 86 | max: max / 1000, 87 | p0_001: histogram.percentile(0.001) / 1000, 88 | p0_01: histogram.percentile(0.01) / 1000, 89 | p0_1: histogram.percentile(0.1) / 1000, 90 | p1: histogram.percentile(1) / 1000, 91 | p2_5: histogram.percentile(2.5) / 1000, 92 | p10: histogram.percentile(10) / 1000, 93 | p25: histogram.percentile(25) / 1000, 94 | p50: histogram.percentile(50) / 1000, 95 | p75: histogram.percentile(75) / 1000, 96 | p90: histogram.percentile(90) / 1000, 97 | p97_5: histogram.percentile(97.5) / 1000, 98 | p99: histogram.percentile(99) / 1000, 99 | p99_9: histogram.percentile(99.9) / 1000, 100 | p99_99: histogram.percentile(99.99) / 1000, 101 | p99_999: histogram.percentile(99.999) / 1000, 102 | }; 103 | } 104 | 105 | static toHistogramIntegerNano(milliseconds: number): number { 106 | return Math.max(1, Math.trunc(milliseconds * 1000)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Piscina from './index'; 2 | 3 | // Used as the require() entry point to maintain existing behavior 4 | export = Piscina; 5 | -------------------------------------------------------------------------------- /src/symbols.ts: -------------------------------------------------------------------------------- 1 | // Internal symbol used to mark Transferable objects returned 2 | // by the Piscina.move() function 3 | export const kMovable = Symbol('Piscina.kMovable'); 4 | export const kWorkerData = Symbol('Piscina.kWorkerData'); 5 | export const kTransferable = Symbol.for('Piscina.transferable'); 6 | export const kValue = Symbol.for('Piscina.valueOf'); 7 | export const kQueueOptions = Symbol.for('Piscina.queueOptions'); 8 | export const kRequestCountField = 0; 9 | export const kResponseCountField = 1; 10 | export const kFieldCount = 2; 11 | -------------------------------------------------------------------------------- /src/task_queue/array_queue.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | import type { TaskQueue, Task } from '.'; 4 | 5 | export class ArrayTaskQueue implements TaskQueue { 6 | tasks: Task[] = [] 7 | 8 | get size () { 9 | return this.tasks.length; 10 | } 11 | 12 | shift (): Task | null { 13 | return this.tasks.shift() ?? null; 14 | } 15 | 16 | push (task: Task): void { 17 | this.tasks.push(task); 18 | } 19 | 20 | remove (task: Task): void { 21 | const index = this.tasks.indexOf(task); 22 | assert.notStrictEqual(index, -1); 23 | this.tasks.splice(index, 1); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/task_queue/common.ts: -------------------------------------------------------------------------------- 1 | import type { kQueueOptions } from '../symbols'; 2 | 3 | export interface TaskQueue { 4 | readonly size: number; 5 | shift(): Task | null; 6 | remove(task: Task): void; 7 | push(task: Task): void; 8 | } 9 | 10 | // Public Interface 11 | export interface PiscinaTask extends Task { 12 | taskId: number; 13 | filename: string; 14 | name: string; 15 | created: number; 16 | isAbortable: boolean; 17 | } 18 | 19 | export interface Task { 20 | readonly [kQueueOptions]: object | null 21 | }; 22 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { MessagePort, Worker } from 'node:worker_threads'; 2 | 3 | import type { READY } from './common'; 4 | import type { kTransferable, kValue } from './symbols'; 5 | 6 | export interface StartupMessage { 7 | filename: string | null 8 | name: string 9 | port: MessagePort 10 | sharedBuffer: Int32Array 11 | atomics: 'async' | 'sync' | 'disabled' 12 | niceIncrement: number 13 | } 14 | 15 | export interface RequestMessage { 16 | taskId: number 17 | task: any 18 | filename: string 19 | name: string 20 | histogramEnabled: number 21 | } 22 | 23 | export interface ReadyMessage { 24 | [READY]: true 25 | } 26 | 27 | export interface ResponseMessage { 28 | taskId: number 29 | result: any 30 | error: Error | null 31 | time: number | null 32 | } 33 | export const commonState = { 34 | isWorkerThread: false, 35 | workerData: undefined 36 | }; 37 | 38 | export interface Transferable { 39 | readonly [kTransferable]: object; 40 | readonly [kValue]: object; 41 | } 42 | 43 | export type ResourceLimits = Worker extends { 44 | resourceLimits?: infer T; 45 | } 46 | ? T 47 | : {}; 48 | export type EnvSpecifier = typeof Worker extends { 49 | new (filename: never, options?: { env: infer T }): Worker; 50 | } 51 | ? T 52 | : never; 53 | -------------------------------------------------------------------------------- /src/worker_pool/balancer/index.ts: -------------------------------------------------------------------------------- 1 | import type { PiscinaTask } from '../../task_queue'; 2 | import type { PiscinaWorker } from '..'; 3 | 4 | export type PiscinaLoadBalancer = ( 5 | task: PiscinaTask, 6 | workers: PiscinaWorker[] 7 | ) => PiscinaWorker | null; // If candidate is passed, it will be used as the result of the load balancer and ingore the command; 8 | 9 | export type LeastBusyBalancerOptions = { 10 | maximumUsage: number; 11 | }; 12 | export function LeastBusyBalancer ( 13 | opts: LeastBusyBalancerOptions 14 | ): PiscinaLoadBalancer { 15 | const { maximumUsage } = opts; 16 | 17 | return (task, workers) => { 18 | let candidate: PiscinaWorker | null = null; 19 | let checkpoint = maximumUsage; 20 | for (const worker of workers) { 21 | if (worker.currentUsage === 0) { 22 | candidate = worker; 23 | break; 24 | } 25 | 26 | if (worker.isRunningAbortableTask) continue; 27 | 28 | if ( 29 | !task.isAbortable && 30 | (worker.currentUsage < checkpoint) 31 | ) { 32 | candidate = worker; 33 | checkpoint = worker.currentUsage; 34 | } 35 | } 36 | 37 | return candidate; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/worker_pool/base.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | export abstract class AsynchronouslyCreatedResource { 4 | onreadyListeners : (() => void)[] | null = []; 5 | ondestroyListeners : (() => void)[] | null = []; 6 | 7 | markAsReady () : void { 8 | const listeners = this.onreadyListeners; 9 | assert(listeners !== null); 10 | this.onreadyListeners = null; 11 | for (const listener of listeners) { 12 | listener(); 13 | } 14 | } 15 | 16 | isReady () : boolean { 17 | return this.onreadyListeners === null; 18 | } 19 | 20 | onReady (fn : () => void) { 21 | if (this.onreadyListeners === null) { 22 | fn(); // Zalgo is okay here. 23 | return; 24 | } 25 | this.onreadyListeners.push(fn); 26 | } 27 | 28 | onDestroy (fn : () => void) { 29 | if (this.ondestroyListeners === null) { 30 | return; 31 | } 32 | 33 | this.ondestroyListeners.push(fn); 34 | } 35 | 36 | markAsDestroyed () { 37 | const listeners = this.ondestroyListeners; 38 | assert(listeners !== null); 39 | this.ondestroyListeners = null; 40 | for (const listener of listeners) { 41 | listener(); 42 | } 43 | } 44 | 45 | isDestroyed () { 46 | return this.ondestroyListeners === null; 47 | } 48 | 49 | abstract currentUsage() : number; 50 | } 51 | 52 | // TODO: this will eventually become an scheduler 53 | export class AsynchronouslyCreatedResourcePool< 54 | T extends AsynchronouslyCreatedResource> { 55 | pendingItems = new Set(); 56 | readyItems = new Set(); 57 | maximumUsage : number; 58 | onAvailableListeners : ((item : T) => void)[]; 59 | onTaskDoneListeners : ((item : T) => void)[]; 60 | 61 | constructor (maximumUsage : number) { 62 | this.maximumUsage = maximumUsage; 63 | this.onAvailableListeners = []; 64 | this.onTaskDoneListeners = []; 65 | } 66 | 67 | add (item : T) { 68 | this.pendingItems.add(item); 69 | item.onReady(() => { 70 | /* istanbul ignore else */ 71 | if (this.pendingItems.has(item)) { 72 | this.pendingItems.delete(item); 73 | this.readyItems.add(item); 74 | this.maybeAvailable(item); 75 | } 76 | }); 77 | } 78 | 79 | delete (item : T) { 80 | this.pendingItems.delete(item); 81 | this.readyItems.delete(item); 82 | } 83 | 84 | * [Symbol.iterator] () { 85 | yield * this.pendingItems; 86 | yield * this.readyItems; 87 | } 88 | 89 | get size () { 90 | return this.pendingItems.size + this.readyItems.size; 91 | } 92 | 93 | maybeAvailable (item : T) { 94 | /* istanbul ignore else */ 95 | if (item.currentUsage() < this.maximumUsage) { 96 | for (const listener of this.onAvailableListeners) { 97 | listener(item); 98 | } 99 | } 100 | } 101 | 102 | onAvailable (fn : (item : T) => void) { 103 | this.onAvailableListeners.push(fn); 104 | } 105 | 106 | taskDone (item : T) { 107 | for (let i = 0; i < this.onTaskDoneListeners.length; i++) { 108 | this.onTaskDoneListeners[i](item); 109 | } 110 | } 111 | 112 | onTaskDone (fn : (item : T) => void) { 113 | this.onTaskDoneListeners.push(fn); 114 | } 115 | 116 | getCurrentUsage (): number { 117 | let inFlight = 0; 118 | for (const worker of this.readyItems) { 119 | const currentUsage = worker.currentUsage(); 120 | 121 | if (Number.isFinite(currentUsage)) inFlight += currentUsage; 122 | } 123 | 124 | return inFlight; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /test/async-context.ts: -------------------------------------------------------------------------------- 1 | import { createHook, executionAsyncId } from 'async_hooks'; 2 | import Piscina from '..'; 3 | import { test } from 'tap'; 4 | import { resolve } from 'path'; 5 | 6 | test('postTask() calls the correct async hooks', async ({ equal }) => { 7 | let taskId; 8 | let initCalls = 0; 9 | let beforeCalls = 0; 10 | let afterCalls = 0; 11 | let resolveCalls = 0; 12 | 13 | const hook = createHook({ 14 | init (id, type) { 15 | if (type === 'Piscina.Task') { 16 | initCalls++; 17 | taskId = id; 18 | } 19 | }, 20 | before (id) { 21 | if (id === taskId) beforeCalls++; 22 | }, 23 | after (id) { 24 | if (id === taskId) afterCalls++; 25 | }, 26 | promiseResolve () { 27 | if (executionAsyncId() === taskId) resolveCalls++; 28 | } 29 | }); 30 | hook.enable(); 31 | 32 | const pool = new Piscina({ 33 | filename: resolve(__dirname, 'fixtures/eval.js') 34 | }); 35 | 36 | await pool.run('42'); 37 | 38 | hook.disable(); 39 | equal(initCalls, 1); 40 | equal(beforeCalls, 1); 41 | equal(afterCalls, 1); 42 | equal(resolveCalls, 1); 43 | }); 44 | -------------------------------------------------------------------------------- /test/atomics-optimization.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | import { test } from 'tap'; 4 | 5 | import Piscina from '..'; 6 | 7 | test('coverage test for Atomics optimization (sync mode)', async ({ equal }) => { 8 | const pool = new Piscina({ 9 | filename: resolve(__dirname, 'fixtures/notify-then-sleep-or.js'), 10 | minThreads: 2, 11 | maxThreads: 2, 12 | concurrentTasksPerWorker: 2, 13 | atomics: 'sync' 14 | }); 15 | 16 | const tasks = []; 17 | let v: number; 18 | 19 | // Post 4 tasks, and wait for all of them to be ready. 20 | const i32array = new Int32Array(new SharedArrayBuffer(4)); 21 | for (let index = 0; index < 4; index++) { 22 | tasks.push(pool.run({ i32array, index })); 23 | } 24 | 25 | // Wait for 2 tasks to enter 'wait' state. 26 | do { 27 | v = Atomics.load(i32array, 0); 28 | if (popcount8(v) >= 2) break; 29 | Atomics.wait(i32array, 0, v); 30 | } while (true); 31 | 32 | // The check above could also be !== 2 but it's hard to get things right 33 | // sometimes and this gives us a nice assertion. Basically, at this point 34 | // exactly 2 tasks should be in Atomics.wait() state. 35 | equal(popcount8(v), 2); 36 | // Wake both tasks up as simultaneously as possible. The other 2 tasks should 37 | // then start executing. 38 | Atomics.store(i32array, 0, 0); 39 | Atomics.notify(i32array, 0, Infinity); 40 | 41 | // Wait for the other 2 tasks to enter 'wait' state. 42 | do { 43 | v = Atomics.load(i32array, 0); 44 | if (popcount8(v) >= 2) break; 45 | Atomics.wait(i32array, 0, v); 46 | } while (true); 47 | 48 | // At this point, the first two tasks are definitely finished and have 49 | // definitely posted results back to the main thread, and the main thread 50 | // has definitely not received them yet, meaning that the Atomics check will 51 | // be used. Making sure that that works is the point of this test. 52 | 53 | // Wake up the remaining 2 tasks in order to make sure that the test finishes. 54 | // Do the same consistency check beforehand as above. 55 | equal(popcount8(v), 2); 56 | Atomics.store(i32array, 0, 0); 57 | Atomics.notify(i32array, 0, Infinity); 58 | 59 | await Promise.all(tasks); 60 | }); 61 | 62 | // Inefficient but straightforward 8-bit popcount 63 | function popcount8 (v : number) : number { 64 | v &= 0xff; 65 | if (v & 0b11110000) return popcount8(v >>> 4) + popcount8(v & 0xb00001111); 66 | if (v & 0b00001100) return popcount8(v >>> 2) + popcount8(v & 0xb00000011); 67 | if (v & 0b00000010) return popcount8(v >>> 1) + popcount8(v & 0xb00000001); 68 | return v; 69 | } 70 | 71 | test('avoids unbounded recursion', async () => { 72 | const pool = new Piscina({ 73 | filename: resolve(__dirname, 'fixtures/simple-isworkerthread.ts'), 74 | minThreads: 2, 75 | maxThreads: 2, 76 | atomics: 'sync' 77 | }); 78 | 79 | const tasks = []; 80 | for (let i = 1; i <= 10000; i++) { 81 | tasks.push(pool.run(null)); 82 | } 83 | 84 | await Promise.all(tasks); 85 | }); 86 | 87 | test('enable async mode', async (t) => { 88 | const pool = new Piscina({ 89 | filename: resolve(__dirname, 'fixtures/eval-params.js'), 90 | minThreads: 1, 91 | maxThreads: 1, 92 | atomics: 'async' 93 | }); 94 | 95 | const bufs = [ 96 | new Int32Array(new SharedArrayBuffer(4)), 97 | new Int32Array(new SharedArrayBuffer(4)), 98 | new Int32Array(new SharedArrayBuffer(4)) 99 | ]; 100 | 101 | const script = ` 102 | setTimeout(() => { Atomics.add(input.shared[0], 0, 1); Atomics.notify(input.shared[0], 0, Infinity); }, 100); 103 | setTimeout(() => { Atomics.add(input.shared[1], 0, 1); Atomics.notify(input.shared[1], 0, Infinity); }, 300); 104 | setTimeout(() => { Atomics.add(input.shared[2], 0, 1); Atomics.notify(input.shared[2], 0, Infinity); }, 500); 105 | 106 | true 107 | `; 108 | 109 | const promise = pool.run({ 110 | code: script, 111 | shared: bufs 112 | }); 113 | 114 | t.plan(2); 115 | 116 | const atResult1 = Atomics.wait(bufs[0], 0, 0); 117 | const atResult2 = Atomics.wait(bufs[1], 0, 0); 118 | const atResult3 = Atomics.wait(bufs[2], 0, 0); 119 | 120 | t.same([atResult1, atResult2, atResult3], ['ok', 'ok', 'ok']); 121 | t.equal(await promise, true); 122 | }); 123 | -------------------------------------------------------------------------------- /test/console-log.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { spawn } from 'node:child_process'; 3 | 4 | import concat from 'concat-stream'; 5 | import { test } from 'tap'; 6 | 7 | test('console.log() calls are not blocked by Atomics.wait() (sync mode)', async ({ equal }) => { 8 | const proc = spawn(process.execPath, [ 9 | ...process.execArgv, resolve(__dirname, 'fixtures/console-log.ts') 10 | ], { 11 | stdio: ['inherit', 'pipe', 'pipe'], 12 | env: { 13 | PISCINA_ENABLE_ASYNC_ATOMICS: '0' 14 | } 15 | }); 16 | 17 | const dataStdout = await new Promise((resolve) => { 18 | proc.stdout.setEncoding('utf8').pipe(concat(resolve)); 19 | }); 20 | const dataStderr = await new Promise((resolve) => { 21 | proc.stderr.setEncoding('utf8').pipe(concat(resolve)); 22 | }); 23 | equal(dataStdout, 'A\n'); 24 | equal(dataStderr, 'B\n'); 25 | }); 26 | -------------------------------------------------------------------------------- /test/fixtures/console-log.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '../..'; 2 | import { resolve } from 'path'; 3 | 4 | const pool = new Piscina({ 5 | filename: resolve(__dirname, 'eval.js'), 6 | maxThreads: 1, 7 | env: { 8 | PISCINA_ENABLE_ASYNC_ATOMICS: process.env.PISCINA_ENABLE_ASYNC_ATOMICS 9 | } 10 | }); 11 | 12 | pool.run('console.log("A"); console.error("B");'); 13 | -------------------------------------------------------------------------------- /test/fixtures/esm-async.mjs: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | const sleep = util.promisify(setTimeout); 3 | 4 | // eslint-disable-next-line no-eval 5 | function handler (code) { return eval(code); } 6 | 7 | async function load () { 8 | await sleep(100); 9 | return handler; 10 | } 11 | 12 | export default load(); 13 | -------------------------------------------------------------------------------- /test/fixtures/esm-export.mjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-eval 2 | export default function (code) { return eval(code); }; 3 | -------------------------------------------------------------------------------- /test/fixtures/eval-async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | // eslint-disable-next-line no-eval 7 | function handler (code) { return eval(code); } 8 | 9 | async function load () { 10 | await sleep(100); 11 | return handler; 12 | } 13 | 14 | module.exports = load(); 15 | -------------------------------------------------------------------------------- /test/fixtures/eval-params.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-eval 2 | module.exports = function (input) { return eval(input.code); }; 3 | -------------------------------------------------------------------------------- /test/fixtures/eval.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-eval 2 | module.exports = function (code) { return eval(code); }; 3 | -------------------------------------------------------------------------------- /test/fixtures/move.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '../..'; 2 | import assert from 'assert'; 3 | import { types } from 'util'; 4 | 5 | export default function (moved) { 6 | if (moved !== undefined) { 7 | assert(types.isAnyArrayBuffer(moved)); 8 | } 9 | return Piscina.move(new ArrayBuffer(10)); 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/multiple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function a () { return 'a'; } 4 | 5 | function b () { return 'b'; } 6 | 7 | a.a = a; 8 | a.b = b; 9 | 10 | module.exports = a; 11 | -------------------------------------------------------------------------------- /test/fixtures/notify-then-sleep-or.js: -------------------------------------------------------------------------------- 1 | // Set the index-th bith in i32array[0], then wait for it to be un-set again. 2 | module.exports = function ({ i32array, index }) { 3 | Atomics.or(i32array, 0, 1 << index); 4 | Atomics.notify(i32array, 0, Infinity); 5 | do { 6 | const v = Atomics.load(i32array, 0); 7 | if (!(v & (1 << index))) break; 8 | Atomics.wait(i32array, 0, v); 9 | } while (true); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/notify-then-sleep-or.ts: -------------------------------------------------------------------------------- 1 | // Set the index-th bith in i32array[0], then wait for it to be un-set again. 2 | module.exports = function ({ i32array, index }) { 3 | Atomics.or(i32array, 0, 1 << index); 4 | Atomics.notify(i32array, 0, Infinity); 5 | do { 6 | const v = Atomics.load(i32array, 0); 7 | if (!(v & (1 << index))) break; 8 | Atomics.wait(i32array, 0, v); 9 | } while (true); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/notify-then-sleep.ts: -------------------------------------------------------------------------------- 1 | module.exports = function (i32array) { 2 | Atomics.store(i32array, 0, 1); 3 | Atomics.notify(i32array, 0, Infinity); 4 | Atomics.wait(i32array, 0, 1); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/resource-limits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const array = []; 5 | while (true) { 6 | array.push([array]); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/send-buffer-then-get-length.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../../dist'); 4 | 5 | let time; 6 | module.exports = { 7 | send: async () => { 8 | const data = new ArrayBuffer(128); 9 | try { 10 | return Piscina.move(data); 11 | } finally { 12 | setTimeout(() => { time = data.byteLength; }, 1000); 13 | } 14 | }, 15 | get: () => { 16 | return time; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/send-transferrable-then-get-length.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Piscina = require('../../dist'); 4 | 5 | class Shared { 6 | constructor (data) { 7 | this.name = 'shared'; 8 | this.data = data; 9 | } 10 | 11 | get [Piscina.transferableSymbol] () { 12 | return [this.data]; 13 | } 14 | 15 | get [Piscina.valueSymbol] () { 16 | return { name: this.name, data: this.data }; 17 | } 18 | 19 | make () { 20 | return Piscina.move(this); 21 | } 22 | } 23 | 24 | let time; 25 | module.exports = { 26 | send: async () => { 27 | const data = new ArrayBuffer(128); 28 | const shared = new Shared(data); 29 | try { 30 | return shared.make(); 31 | } finally { 32 | setTimeout(() => { time = data.byteLength; }, 1000); 33 | } 34 | }, 35 | get: () => { 36 | return time; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /test/fixtures/simple-isworkerthread-named-import.ts: -------------------------------------------------------------------------------- 1 | import { isWorkerThread } from '../..'; 2 | import assert from 'assert'; 3 | 4 | assert.strictEqual(isWorkerThread, true); 5 | 6 | export default function () { return 'done'; } 7 | -------------------------------------------------------------------------------- /test/fixtures/simple-isworkerthread.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '../..'; 2 | import assert from 'assert'; 3 | 4 | assert.strictEqual(Piscina.isWorkerThread, true); 5 | 6 | export default function () { return 'done'; } 7 | -------------------------------------------------------------------------------- /test/fixtures/simple-workerdata-named-import.ts: -------------------------------------------------------------------------------- 1 | import { workerData } from '../..'; 2 | import assert from 'assert'; 3 | 4 | assert.strictEqual(workerData, 'ABC'); 5 | 6 | export default function () { return 'done'; } 7 | -------------------------------------------------------------------------------- /test/fixtures/simple-workerdata.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '../..'; 2 | import assert from 'assert'; 3 | 4 | assert.strictEqual(Piscina.workerData, 'ABC'); 5 | 6 | export default function () { return 'done'; } 7 | -------------------------------------------------------------------------------- /test/fixtures/sleep.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const sleep = promisify(setTimeout); 5 | 6 | const buf = new Uint32Array(new SharedArrayBuffer(4)); 7 | 8 | module.exports = async ({ time = 100, a }) => { 9 | await sleep(time); 10 | const ret = Atomics.exchange(buf, 0, a); 11 | return ret; 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/vm.js: -------------------------------------------------------------------------------- 1 | // worker.js 2 | const vm = require('vm'); 3 | 4 | module.exports = ({ payload, context }) => { 5 | const script = new vm.Script(payload); 6 | script.runInNewContext(context); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/wait-for-notify.js: -------------------------------------------------------------------------------- 1 | module.exports = function (i32array) { 2 | Atomics.wait(i32array, 0, 0); 3 | Atomics.store(i32array, 0, -1); 4 | Atomics.notify(i32array, 0, Infinity); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/wait-for-notify.ts: -------------------------------------------------------------------------------- 1 | module.exports = function (i32array) { 2 | Atomics.wait(i32array, 0, 0); 3 | Atomics.store(i32array, 0, -1); 4 | Atomics.notify(i32array, 0, Infinity); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/wait-for-others.ts: -------------------------------------------------------------------------------- 1 | import { threadId } from 'worker_threads'; 2 | 3 | module.exports = async function ([i32array, n]) { 4 | Atomics.add(i32array, 0, 1); 5 | Atomics.notify(i32array, 0, Infinity); 6 | let lastSeenValue; 7 | while ((lastSeenValue = Atomics.load(i32array, 0)) < n) { 8 | Atomics.wait(i32array, 0, lastSeenValue); 9 | } 10 | return threadId; 11 | }; 12 | -------------------------------------------------------------------------------- /test/generics.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import Piscina from '..'; 3 | import { test } from 'tap'; 4 | 5 | test('Piscina works', async ({ equal }) => { 6 | const worker = new Piscina({ 7 | filename: resolve(__dirname, 'fixtures/eval.js') 8 | }); 9 | 10 | const result: number = await worker.run('Promise.resolve(42)'); 11 | equal(result, 42); 12 | }); 13 | 14 | test('Piscina with no generic works', async ({ equal }) => { 15 | const worker = new Piscina({ 16 | filename: resolve(__dirname, 'fixtures/eval.js') 17 | }); 18 | 19 | const result = await worker.run('Promise.resolve("Hello, world!")'); 20 | equal(result, 'Hello, world!'); 21 | }); 22 | 23 | test('Piscina typescript complains when invalid Task is supplied as wrong type', async ({ equal }) => { 24 | const worker = new Piscina({ 25 | filename: resolve(__dirname, 'fixtures/eval.js') 26 | }); 27 | 28 | // @ts-expect-error complains due to invalid Task being number when expecting string 29 | const result = await worker.run(42); 30 | 31 | equal(result, 42); 32 | }); 33 | 34 | test('Piscina typescript complains when assigning Result to wrong type', async ({ equal }) => { 35 | const worker = new Piscina({ 36 | filename: resolve(__dirname, 'fixtures/eval.js') 37 | }); 38 | 39 | // @ts-expect-error complains due to expecting a number but being assigned to a string 40 | const result: string = await worker.run('Promise.resolve(42)'); 41 | equal(result, 42); 42 | }); 43 | -------------------------------------------------------------------------------- /test/idle-timeout.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | import { promisify } from 'util'; 5 | 6 | const delay = promisify(setTimeout); 7 | 8 | test('idle timeout will let go of threads early', async ({ equal }) => { 9 | const pool = new Piscina({ 10 | filename: resolve(__dirname, 'fixtures/wait-for-others.ts'), 11 | idleTimeout: 500, 12 | minThreads: 1, 13 | maxThreads: 2 14 | }); 15 | 16 | equal(pool.threads.length, 1); 17 | const buffer = new Int32Array(new SharedArrayBuffer(4)); 18 | 19 | const firstTasks = [ 20 | pool.run([buffer, 2]), 21 | pool.run([buffer, 2]) 22 | ]; 23 | equal(pool.threads.length, 2); 24 | 25 | const earlyThreadIds = await Promise.all(firstTasks); 26 | equal(pool.threads.length, 2); 27 | 28 | await delay(2000); 29 | equal(pool.threads.length, 1); 30 | 31 | const secondTasks = [ 32 | pool.run([buffer, 4]), 33 | pool.run([buffer, 4]) 34 | ]; 35 | equal(pool.threads.length, 2); 36 | 37 | const lateThreadIds = await Promise.all(secondTasks); 38 | 39 | // One thread should have been idle in between and exited, one should have 40 | // been reused. 41 | equal(earlyThreadIds.length, 2); 42 | equal(lateThreadIds.length, 2); 43 | equal(new Set([...earlyThreadIds, ...lateThreadIds]).size, 3); 44 | }); 45 | 46 | test('idle timeout will not let go of threads if Infinity is used as the value', async ({ deepEqual, equal }) => { 47 | const pool = new Piscina({ 48 | filename: resolve(__dirname, 'fixtures/wait-for-others.ts'), 49 | idleTimeout: Infinity, 50 | minThreads: 1, 51 | maxThreads: 2 52 | }); 53 | equal(pool.threads.length, 1); 54 | const buffer = new Int32Array(new SharedArrayBuffer(4)); 55 | 56 | const firstTasks = [ 57 | pool.run([buffer, 2]), 58 | pool.run([buffer, 2]) 59 | ]; 60 | equal(pool.threads.length, 2); 61 | 62 | const earlyThreadIds = await Promise.all(firstTasks); 63 | equal(pool.threads.length, 2); 64 | 65 | await delay(2000); 66 | equal(pool.threads.length, 2); 67 | 68 | const secondTasks = [ 69 | pool.run([buffer, 4]), 70 | pool.run([buffer, 4]), 71 | ]; 72 | equal(pool.threads.length, 2); 73 | 74 | 75 | 76 | const lateThreadIds = await Promise.all(secondTasks); 77 | deepEqual(earlyThreadIds, lateThreadIds); 78 | 79 | await Promise.all([pool.run([buffer, 6]), pool.run([buffer, 6]), pool.run([buffer, 6])]); 80 | equal(pool.threads.length, 2); 81 | }); -------------------------------------------------------------------------------- /test/issue-513.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | 5 | test('pool will maintain run and wait time histograms', async ({ 6 | equal, 7 | fail 8 | }) => { 9 | const pool = new Piscina({ 10 | filename: resolve(__dirname, 'fixtures/vm.js') 11 | }); 12 | 13 | try { 14 | await pool.run({ payload: 'throw new Error("foo")' }); 15 | fail('Expected an error'); 16 | } catch (error) { 17 | equal(error.message, 'foo'); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /test/load-with-esm.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap'; 2 | 3 | const importESM : (specifier : string) => Promise = 4 | // eslint-disable-next-line no-eval 5 | eval('(specifier) => import(specifier)'); 6 | 7 | test('Piscina is default export', {}, async ({ equal }) => { 8 | equal((await importESM('piscina')).default, require('../')); 9 | }); 10 | 11 | test('Exports match own property names', {}, async ({ strictSame }) => { 12 | // Check that version, workerData, etc. are re-exported. 13 | const exported = new Set(Object.getOwnPropertyNames(await importESM('piscina'))); 14 | const required = new Set(Object.getOwnPropertyNames(require('../'))); 15 | 16 | // Remove constructor properties + default export. 17 | for (const k of ['prototype', 'length', 'name']) required.delete(k); 18 | exported.delete('default'); 19 | 20 | strictSame(exported, required); 21 | }); 22 | -------------------------------------------------------------------------------- /test/messages.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | import { once } from 'events'; 5 | 6 | test('Pool receive message from workers', async ({ equal }) => { 7 | const pool = new Piscina({ 8 | filename: resolve(__dirname, 'fixtures/eval.js') 9 | }); 10 | 11 | const messagePromise = once(pool, 'message'); 12 | 13 | const taskResult = pool.run(` 14 | require('worker_threads').parentPort.postMessage("some message"); 15 | 42 16 | `); 17 | equal(await taskResult, 42); 18 | equal((await messagePromise)[0], 'some message'); 19 | }); 20 | -------------------------------------------------------------------------------- /test/move-test.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { 3 | isMovable, 4 | markMovable, 5 | isTransferable 6 | } from '../dist/common'; 7 | import { test } from 'tap'; 8 | import { types } from 'util'; 9 | import { MessageChannel, MessagePort } from 'worker_threads'; 10 | import { resolve } from 'path'; 11 | 12 | const { 13 | transferableSymbol, 14 | valueSymbol 15 | } = Piscina; 16 | 17 | test('Marking an object as movable works as expected', async ({ ok }) => { 18 | const obj : any = { 19 | get [transferableSymbol] () : object { return {}; }, 20 | get [valueSymbol] () : object { return {}; } 21 | }; 22 | ok(isTransferable(obj)); 23 | ok(!isMovable(obj)); // It's not movable initially 24 | markMovable(obj); 25 | ok(isMovable(obj)); // It is movable now 26 | }); 27 | 28 | test('Marking primitives and null works as expected', async ({ equal }) => { 29 | equal(Piscina.move(null), null); 30 | equal(Piscina.move(1 as any), 1); 31 | equal(Piscina.move(false as any), false); 32 | equal(Piscina.move('test' as any), 'test'); 33 | }); 34 | 35 | test('Using Piscina.move() returns a movable object', async ({ ok }) => { 36 | const obj : any = { 37 | get [transferableSymbol] () : object { return {}; }, 38 | get [valueSymbol] () : object { return {}; } 39 | }; 40 | ok(!isMovable(obj)); // It's not movable initially 41 | const movable = Piscina.move(obj); 42 | ok(isMovable(movable)); // It is movable now 43 | }); 44 | 45 | test('Using ArrayBuffer works as expected', async ({ ok, equal }) => { 46 | const ab = new ArrayBuffer(5); 47 | const movable = Piscina.move(ab); 48 | ok(isMovable(movable)); 49 | ok(types.isAnyArrayBuffer(movable[valueSymbol])); 50 | ok(types.isAnyArrayBuffer(movable[transferableSymbol])); 51 | equal(movable[transferableSymbol], ab); 52 | }); 53 | 54 | test('Using TypedArray works as expected', async ({ ok, equal }) => { 55 | const ab = new Uint8Array(5); 56 | const movable = Piscina.move(ab); 57 | ok(isMovable(movable)); 58 | ok((types as any).isArrayBufferView(movable[valueSymbol])); 59 | ok(types.isAnyArrayBuffer(movable[transferableSymbol])); 60 | equal(movable[transferableSymbol], ab.buffer); 61 | }); 62 | 63 | test('Using MessagePort works as expected', async ({ ok, equal }) => { 64 | const mc = new MessageChannel(); 65 | const movable = Piscina.move(mc.port1); 66 | ok(isMovable(movable)); 67 | ok(movable[valueSymbol] instanceof MessagePort); 68 | ok(movable[transferableSymbol] instanceof MessagePort); 69 | equal(movable[transferableSymbol], mc.port1); 70 | }); 71 | 72 | test('Moving works', async ({ equal, ok }) => { 73 | const pool = new Piscina({ 74 | filename: resolve(__dirname, 'fixtures/move.ts') 75 | }); 76 | 77 | { 78 | // Test with empty transferList 79 | const ab = new ArrayBuffer(10); 80 | const ret = await pool.run(Piscina.move(ab)); 81 | equal(ab.byteLength, 0); // It was moved 82 | ok(types.isAnyArrayBuffer(ret)); 83 | } 84 | 85 | { 86 | // Test with empty transferList 87 | const ab = new ArrayBuffer(10); 88 | const ret = await pool.run(Piscina.move(ab), { transferList: [] }); 89 | equal(ab.byteLength, 0); // It was moved 90 | ok(types.isAnyArrayBuffer(ret)); 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /test/nice.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { getCurrentProcessPriority, WindowsThreadPriority } from '@napi-rs/nice'; 3 | import { resolve } from 'path'; 4 | import { test } from 'tap'; 5 | 6 | test('niceness - Linux:', { skip: process.platform !== 'linux' }, scope => { 7 | scope.plan(2); 8 | 9 | scope.test('can set niceness for threads on Linux', async ({ equal }) => { 10 | const worker = new Piscina({ 11 | filename: resolve(__dirname, 'fixtures/eval.js'), 12 | niceIncrement: 5 13 | }); 14 | 15 | // ts-ignore because the dependency is not installed on Windows. 16 | // @ts-ignore 17 | const currentNiceness = getCurrentProcessPriority(); 18 | const result = await worker.run('require("@napi-rs/nice").getCurrentProcessPriority()'); 19 | // niceness is capped to 19 on Linux. 20 | const expected = Math.min(currentNiceness + 5, 19); 21 | equal(result, expected); 22 | }); 23 | 24 | scope.test('setting niceness never does anything bad', async ({ equal }) => { 25 | const worker = new Piscina({ 26 | filename: resolve(__dirname, 'fixtures/eval.js'), 27 | niceIncrement: 5 28 | }); 29 | 30 | const result = await worker.run('42'); 31 | equal(result, 42); 32 | }); 33 | }); 34 | 35 | test('niceness - Windows', { 36 | skip: process.platform !== 'win32' 37 | }, scope => { 38 | scope.plan(1); 39 | scope.test('can set niceness for threads on Windows', async ({ equal }) => { 40 | const worker = new Piscina({ 41 | filename: resolve(__dirname, 'fixtures/eval.js'), 42 | niceIncrement: WindowsThreadPriority.ThreadPriorityAboveNormal 43 | }); 44 | 45 | const result = await worker.run('require("@napi-rs/nice").getCurrentProcessPriority()'); 46 | 47 | equal(result, WindowsThreadPriority.ThreadPriorityAboveNormal); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/pool-close.ts: -------------------------------------------------------------------------------- 1 | import { once } from 'node:events'; 2 | import { resolve } from 'node:path'; 3 | 4 | import { test } from 'tap'; 5 | 6 | import Piscina from '..'; 7 | 8 | test('close()', async (t) => { 9 | t.test('no pending tasks', async (t) => { 10 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/sleep.js') }); 11 | await pool.close(); 12 | t.pass('pool closed successfully'); 13 | }); 14 | 15 | t.test('no pending tasks (with minThreads=0)', async (t) => { 16 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/sleep.js'), minThreads: 0 }); 17 | await pool.close(); 18 | t.pass('pool closed successfully'); 19 | }); 20 | }); 21 | 22 | test('queued tasks waits for all tasks to complete', async (t) => { 23 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/sleep.js'), maxThreads: 1 }); 24 | 25 | const task1 = pool.run({ time: 100 }); 26 | const task2 = pool.run({ time: 100 }); 27 | 28 | setImmediate(() => t.resolves(pool.close(), 'close is resolved when all running tasks are completed')); 29 | 30 | await Promise.all([ 31 | t.resolves(once(pool, 'close'), 'handler is called when pool is closed'), 32 | t.resolves(task1, 'complete running task'), 33 | t.resolves(task2, 'complete running task') 34 | ]); 35 | }); 36 | 37 | test('abort any task enqueued during closing up', async (t) => { 38 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/sleep.js'), maxThreads: 1 }); 39 | 40 | setImmediate(() => { 41 | t.resolves(pool.close(), 'close is resolved when running tasks are completed'); 42 | t.resolves(pool.run({ time: 1000 }).then(null, err => { 43 | t.equal(err.message, 'The task has been aborted'); 44 | t.equal(err.cause, 'queue is being terminated'); 45 | })); 46 | }); 47 | 48 | await t.resolves(pool.run({ time: 100 }), 'complete running task'); 49 | }); 50 | 51 | test('force: queued tasks waits for all tasks already running and aborts tasks that are not started yet', async (t) => { 52 | const pool = new Piscina({ filename: resolve(__dirname, 'fixtures/sleep.js'), maxThreads: 1, concurrentTasksPerWorker: 2 }); 53 | 54 | const task1 = pool.run({ time: 1000 }); 55 | const task2 = pool.run({ time: 1000 }); 56 | // const task3 = pool.run({ time: 100 }); 57 | // const task4 = pool.run({ time: 100 }); 58 | 59 | t.plan(6); 60 | 61 | t.resolves(pool.close({ force: true })); 62 | t.resolves(once(pool, 'close'), 'handler is called when pool is closed'); 63 | t.resolves(task1, 'complete running task'); 64 | t.resolves(task2, 'complete running task'); 65 | t.rejects(pool.run({ time: 100 }), /The task has been aborted/, 'abort task that are not started yet'); 66 | t.rejects(pool.run({ time: 100 }), /The task has been aborted/, 'abort task that are not started yet'); 67 | 68 | await task1; 69 | await task2; 70 | }); 71 | 72 | test('timed out close operation destroys the pool', async (t) => { 73 | const pool = new Piscina({ 74 | filename: resolve(__dirname, 'fixtures/sleep.js'), 75 | maxThreads: 1, 76 | closeTimeout: 500 77 | }); 78 | 79 | const task1 = pool.run({ time: 5000 }); 80 | const task2 = pool.run({ time: 5000 }); 81 | 82 | setImmediate(() => t.resolves(pool.close(), 'close is resolved on timeout')); 83 | 84 | await Promise.all([ 85 | t.resolves(once(pool, 'error'), 'error handler is called on timeout'), 86 | t.rejects(task1, /Terminating worker thread/, 'task is aborted due to timeout'), 87 | t.rejects(task2, /Terminating worker thread/, 'task is aborted due to timeout') 88 | ]); 89 | }); 90 | -------------------------------------------------------------------------------- /test/pool-destroy.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | 5 | test('can destroy pool while tasks are running', async ({ rejects }) => { 6 | const pool = new Piscina({ 7 | filename: resolve(__dirname, 'fixtures/eval.js') 8 | }); 9 | setImmediate(() => pool.destroy()); 10 | await rejects(pool.run('while(1){}'), /Terminating worker thread/); 11 | }); 12 | -------------------------------------------------------------------------------- /test/pool.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | import { test } from 'tap'; 4 | 5 | import Piscina from '../dist'; 6 | 7 | test('workerCreate/workerDestroy should be emitted while managing worker lifecycle', async t => { 8 | let index = 0; 9 | // Its expected to have one task get balanced twice due to the load balancer distribution 10 | // first task enters, its distributed; second is enqueued, once first is done, second is distributed and normalizes 11 | t.plan(2); 12 | let newWorkers = 0; 13 | let destroyedWorkers = 0; 14 | const pool = new Piscina({ 15 | filename: resolve(__dirname, 'fixtures/eval.js'), 16 | maxThreads: 3, 17 | minThreads: 3, 18 | concurrentTasksPerWorker: 1, 19 | loadBalancer (_task, workers) { 20 | // Verify distribution to properly test this feature 21 | const candidate = workers[index++ % workers.length]; 22 | if (candidate != null && candidate.currentUsage >= 1) { 23 | return null; 24 | } 25 | 26 | return candidate; 27 | } 28 | }); 29 | 30 | pool.on('workerCreate', () => { 31 | newWorkers++; 32 | }); 33 | 34 | pool.on('workerDestroy', () => { 35 | destroyedWorkers++; 36 | }); 37 | 38 | const tasks = []; 39 | const controller = new AbortController(); 40 | const signal = controller.signal; 41 | tasks.push(pool.run('while (true) {}', { 42 | signal 43 | })); 44 | 45 | for (let n = 0; n < 10; n++) { 46 | tasks.push(pool.run('new Promise(resolve => setTimeout(resolve, 500))')); 47 | } 48 | 49 | controller.abort(); 50 | await Promise.allSettled(tasks); 51 | await pool.close(); 52 | t.equal(destroyedWorkers, 4); 53 | t.equal(newWorkers, 4); 54 | }); 55 | -------------------------------------------------------------------------------- /test/test-is-buffer-transferred.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | 5 | function wait () { 6 | return new Promise((resolve) => setTimeout(resolve, 1500)); 7 | } 8 | 9 | test('transferable objects must be transferred', async ({ equal }) => { 10 | const pool = new Piscina({ 11 | filename: resolve(__dirname, 'fixtures/send-buffer-then-get-length.js'), 12 | atomics: 'disabled' 13 | }); 14 | await pool.run({}, { name: 'send' }); 15 | await wait(); 16 | const after = await pool.run({}, { name: 'get' }); 17 | equal(after, 0); 18 | }); 19 | 20 | test('objects that implement transferable must be transferred', async ({ 21 | equal 22 | }) => { 23 | const pool = new Piscina({ 24 | filename: resolve( 25 | __dirname, 26 | 'fixtures/send-transferrable-then-get-length.js' 27 | ), 28 | atomics: 'disabled' 29 | }); 30 | await pool.run({}, { name: 'send' }); 31 | await wait(); 32 | const after = await pool.run({}, { name: 'get' }); 33 | equal(after, 0); 34 | }); 35 | -------------------------------------------------------------------------------- /test/test-resourcelimits.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | 5 | test('resourceLimits causes task to reject', async ({ equal, rejects }) => { 6 | const worker = new Piscina({ 7 | filename: resolve(__dirname, 'fixtures/resource-limits.js'), 8 | resourceLimits: { 9 | maxOldGenerationSizeMb: 16, 10 | maxYoungGenerationSizeMb: 4, 11 | codeRangeSizeMb: 16 12 | } 13 | }); 14 | worker.on('error', () => { 15 | // Ignore any additional errors that may occur. 16 | // This may happen because when the Worker is 17 | // killed a new worker is created that may hit 18 | // the memory limits immediately. When that 19 | // happens, there is no associated Promise to 20 | // reject so we emit an error event instead. 21 | // We don't care so much about that here. We 22 | // could potentially avoid the issue by setting 23 | // higher limits above but rather than try to 24 | // guess at limits that may work consistently, 25 | // let's just ignore the additional error for 26 | // now. 27 | }); 28 | const limits : any = worker.options.resourceLimits; 29 | equal(limits.maxOldGenerationSizeMb, 16); 30 | equal(limits.maxYoungGenerationSizeMb, 4); 31 | equal(limits.codeRangeSizeMb, 16); 32 | rejects(worker.run(null), 33 | /Worker terminated due to reaching memory limit: JS heap out of memory/); 34 | }); 35 | -------------------------------------------------------------------------------- /test/test-uncaught-exception-from-handler.ts: -------------------------------------------------------------------------------- 1 | import Piscina from '..'; 2 | import { test } from 'tap'; 3 | import { resolve } from 'path'; 4 | import { once } from 'events'; 5 | 6 | test('uncaught exception resets Worker', async ({ rejects }) => { 7 | const pool = new Piscina({ 8 | filename: resolve(__dirname, 'fixtures/eval.js') 9 | }); 10 | await rejects(pool.run('throw new Error("not_caught")'), /not_caught/); 11 | }); 12 | 13 | test('uncaught exception in immediate resets Worker', async ({ rejects }) => { 14 | const pool = new Piscina({ 15 | filename: resolve(__dirname, 'fixtures/eval.js') 16 | }); 17 | await rejects( 18 | pool.run(` 19 | setImmediate(() => { throw new Error("not_caught") }); 20 | new Promise(() => {}) /* act as if we were doing some work */ 21 | `), /not_caught/); 22 | }); 23 | 24 | test('uncaught exception in immediate after task yields error event', async ({ equal }) => { 25 | const pool = new Piscina({ 26 | filename: resolve(__dirname, 'fixtures/eval.js'), 27 | maxThreads: 1, 28 | atomics: 'disabled' 29 | }); 30 | 31 | const errorEvent : Promise = once(pool, 'error'); 32 | 33 | const taskResult = pool.run(` 34 | setTimeout(() => { throw new Error("not_caught") }, 500); 35 | 42 36 | `); 37 | 38 | equal(await taskResult, 42); 39 | 40 | // Hack a bit to make sure we get the 'exit'/'error' events. 41 | equal(pool.threads.length, 1); 42 | pool.threads[0].ref(); 43 | 44 | // This is the main assertion here. 45 | equal((await errorEvent)[0].message, 'not_caught'); 46 | }); 47 | 48 | test('exiting process resets worker', async ({ not, rejects }) => { 49 | const pool = new Piscina({ 50 | filename: resolve(__dirname, 'fixtures/eval.js'), 51 | minThreads: 1 52 | }); 53 | const originalThreadId = pool.threads[0].threadId; 54 | await rejects(pool.run('process.exit(1);'), /worker exited with code: 1/); 55 | const newThreadId = pool.threads[0].threadId; 56 | not(originalThreadId, newThreadId); 57 | }); 58 | 59 | test('exiting process in immediate after task errors next task and resets worker', async ({ equal, not, rejects }) => { 60 | const pool = new Piscina({ 61 | filename: resolve(__dirname, 'fixtures/eval-async.js'), 62 | minThreads: 1 63 | }); 64 | 65 | const originalThreadId = pool.threads[0].threadId; 66 | const taskResult = await pool.run(` 67 | setTimeout(() => { process.exit(1); }, 50); 68 | 42 69 | `); 70 | equal(taskResult, 42); 71 | 72 | await rejects(pool.run(` 73 | 'use strict'; 74 | 75 | const { promisify } = require('util'); 76 | const sleep = promisify(setTimeout); 77 | async function _() { 78 | await sleep(1000); 79 | return 42 80 | } 81 | _(); 82 | `), /worker exited with code: 1/); 83 | const secondThreadId = pool.threads[0].threadId; 84 | 85 | not(originalThreadId, secondThreadId); 86 | }); 87 | -------------------------------------------------------------------------------- /test/thread-count.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { cpus } from 'node:os'; 3 | import { once } from 'node:events'; 4 | import Piscina from '..'; 5 | import { test } from 'tap'; 6 | 7 | test('will start with minThreads and max out at maxThreads', { only: true }, async ({ equal, rejects }) => { 8 | const pool = new Piscina({ 9 | filename: resolve(__dirname, 'fixtures/eval.js'), 10 | minThreads: 2, 11 | maxThreads: 4, 12 | concurrentTasksPerWorker: 1 13 | }); 14 | let counter = 0; 15 | 16 | pool.on('workerCreate', () => { 17 | counter++; 18 | }); 19 | 20 | equal(pool.threads.length, 2); 21 | 22 | rejects(pool.run('while(true) {}')); 23 | rejects(pool.run('while(true) {}')); 24 | 25 | // #3 26 | rejects(pool.run('while(true) {}')); 27 | await once(pool, 'workerCreate'); 28 | 29 | // #4 30 | rejects(pool.run('while(true) {}')); 31 | await once(pool, 'workerCreate'); 32 | 33 | // #4 - as spawn does not happen synchronously anymore, we wait for the signal once more 34 | rejects(pool.run('while(true) {}')); 35 | await once(pool, 'workerCreate'); 36 | 37 | equal(pool.threads.length, 4); 38 | await pool.destroy(); 39 | equal(pool.threads.length, 0); 40 | equal(counter, 4); 41 | }); 42 | 43 | test('low maxThreads sets minThreads', async ({ equal }) => { 44 | const pool = new Piscina({ 45 | filename: resolve(__dirname, 'fixtures/eval.js'), 46 | maxThreads: 1 47 | }); 48 | equal(pool.threads.length, 1); 49 | equal(pool.options.minThreads, 1); 50 | equal(pool.options.maxThreads, 1); 51 | }); 52 | 53 | test('high minThreads sets maxThreads', { 54 | skip: cpus().length > 8 55 | }, async ({ equal }) => { 56 | const pool = new Piscina({ 57 | filename: resolve(__dirname, 'fixtures/eval.js'), 58 | minThreads: 16 59 | }); 60 | equal(pool.threads.length, 16); 61 | equal(pool.options.minThreads, 16); 62 | equal(pool.options.maxThreads, 16); 63 | }); 64 | 65 | test('conflicting min/max threads is error', async ({ throws }) => { 66 | throws(() => new Piscina({ 67 | minThreads: 16, 68 | maxThreads: 8 69 | }), /options.minThreads and options.maxThreads must not conflict/); 70 | }); 71 | 72 | test('thread count should be 0 upon destruction', async ({ equal }) => { 73 | const pool = new Piscina({ 74 | filename: resolve(__dirname, 'fixtures/eval.js'), 75 | minThreads: 2, 76 | maxThreads: 4 77 | }); 78 | equal(pool.threads.length, 2); 79 | await pool.destroy(); 80 | equal(pool.threads.length, 0); 81 | }); 82 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "strict": false, 5 | "noImplicitAny": false 6 | }, 7 | "include": [ 8 | "./**/*.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/workers.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | import { test } from 'tap'; 4 | 5 | import Piscina from '../dist'; 6 | 7 | test('workers are marked as destroyed if destroyed', async t => { 8 | let index = 0; 9 | // Its expected to have one task get balanced twice due to the load balancer distribution 10 | // first task enters, its distributed; second is enqueued, once first is done, second is distributed and normalizes 11 | t.plan(4); 12 | let workersFirstRound = []; 13 | let workersSecondRound = []; 14 | const pool = new Piscina({ 15 | filename: resolve(__dirname, 'fixtures/eval.js'), 16 | minThreads: 2, 17 | maxThreads: 2, 18 | concurrentTasksPerWorker: 1, 19 | loadBalancer (_task, workers) { 20 | if (workersFirstRound.length === 0) { 21 | workersFirstRound = workers; 22 | workersSecondRound = workers; 23 | } else if ( 24 | workersFirstRound[0].id !== workers[0].id 25 | ) { 26 | workersSecondRound = workers; 27 | } 28 | // Verify distribution to properly test this feature 29 | const candidate = workers[index++ % workers.length]; 30 | 31 | if (candidate.currentUsage !== 0 && !candidate.isRunningAbortableTask) { 32 | return null; 33 | } 34 | 35 | return candidate; 36 | } 37 | }); 38 | 39 | const tasks = []; 40 | const controller = new AbortController(); 41 | const signal = controller.signal; 42 | tasks.push(pool.run('while (true) {}', { 43 | signal 44 | })); 45 | 46 | for (let n = 0; n < 5; n++) { 47 | tasks.push(pool.run('new Promise(resolve => setTimeout(resolve, 500))')); 48 | } 49 | 50 | controller.abort(); 51 | await Promise.allSettled(tasks); 52 | t.strictNotSame(workersFirstRound, workersSecondRound); 53 | t.equal(workersFirstRound.length, 2); 54 | t.ok(workersFirstRound[0].destroyed); 55 | t.notOk(workersFirstRound[0].terminating); 56 | }); 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["es2019"], 7 | "outDir": "dist", 8 | "rootDir": "./src", 9 | "declaration": true, 10 | "sourceMap": true, 11 | 12 | "strict": true, 13 | 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "esModuleInterop": true, 19 | 20 | "resolveJsonModule": true, /* Include modules imported with '.json' extension */ 21 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 22 | }, 23 | "include": ["src"], 24 | } 25 | --------------------------------------------------------------------------------