Repository: parcel-bundler/parcel
Pull request #5249 (2020-10-16 03:26:27)

Dispose of WorkerFarm resources on watch end
This:
* Adds an internal method to `Parcel`, `_end()`, which handles persisting the graphs as well as freeing WorkerFarm resources. This method is called at the end of `run()` and when the last subscriber unsubscribes from `watch()`. This method un-initializes Parcel, and Parcel can be re-initialized on a subsequent call to run or watch.
* Prevents Parcel from initializing using a WorkerFarm in the process of ending.
* Adds tests for `Parcel`, asserting that it creates WorkerFarms when necessary, does not dispose of passed worker farms, and disposes of workerfarm resources properly.

Test Plan:
* `yarn test`
* Verify that Parcel shuts down promptly during transformation.

height[bot]: > Link [Height](https://parcel.height.app) tasks by mentioning a task ID in the pull request title or description, commit messages, or comments.
>
> 💡Tip: You can also use "Close T-X" to automatically close a task when the pull request is merged.
<!-- Sent by Height -->
parcel-benchmark: ## Benchmark Results
<details><summary>Kitchen Sink ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 9.22s | -51.00ms |
| Cached | 3.80s | -30.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/legacy/parcel.d5807e82.webp | 102.94kb | +0.00b | 410.00ms | +191.00ms ⚠️ |
| dist/modern/parcel.d5807e82.webp | 102.94kb | +0.00b | 293.00ms | -53.00ms 🚀 |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/legacy/parcel.d5807e82.webp | 102.94kb | +0.00b | 391.00ms | -23.00ms 🚀 |
| dist/modern/parcel.d5807e82.webp | 102.94kb | +0.00b | 317.00ms | +65.00ms ⚠️ |
| dist/legacy/index.45cccf70.js | 1.10kb | +0.00b | 476.00ms | -24.00ms 🚀 |
| dist/modern/index.1964d1f6.js | 1.10kb | +0.00b | 490.00ms | +133.00ms ⚠️ |
| dist/legacy/index.html | 701.00b | +0.00b | 476.00ms | -25.00ms 🚀 |
| dist/modern/index.html | 701.00b | +0.00b | 490.00ms | +133.00ms ⚠️ |
| dist/legacy/index.b3acc8eb.css | 77.00b | +0.00b | 476.00ms | -24.00ms 🚀 |
| dist/modern/index.7bc22a8b.css | 77.00b | +0.00b | 490.00ms | +134.00ms ⚠️ |

</p></details>

<details><summary>React HackerNews ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 35.28s | +1.84s ⚠️ |
| Cached | 3.92s | +75.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/PermalinkedComment.9c5ad065.js | 4.22kb | +0.00b | 14.41s | +6.93s ⚠️ |
| dist/UserProfile.c1927888.js | 1.62kb | +0.00b | 14.41s | +6.93s ⚠️ |
| dist/NotFound.f8a0616d.js | 387.00b | +0.00b | 1.29s | +143.00ms ⚠️ |
| dist/logo.24c8bf9e.png | 274.00b | +0.00b | 231.00ms | +30.00ms ⚠️ |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.js | 478.81kb | +0.00b | 287.00ms | +55.00ms ⚠️ |
| dist/PermalinkedComment.9c5ad065.js | 4.22kb | +0.00b | 287.00ms | +55.00ms ⚠️ |
| dist/UserProfile.c1927888.js | 1.62kb | +0.00b | 287.00ms | +55.00ms ⚠️ |
| dist/NotFound.f8a0616d.js | 387.00b | +0.00b | 191.00ms | -80.00ms 🚀 |
| dist/logo.24c8bf9e.png | 274.00b | +0.00b | 227.00ms | +72.00ms ⚠️ |

</p></details>

<details><summary>AtlasKit Editor ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 3.45m | -3.30s |
| Cached | 8.47s | -89.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.accd8b81.js | 2.72mb | -3.00b 🚀 | 1.82m | -502.00ms |
| dist/popup.6c453e3c.js | 169.96kb | +0.00b | 38.21s | -23.59s 🚀 |
| dist/card.645a5c17.js | 55.01kb | +0.00b | 16.72s | +1.80s ⚠️ |
| dist/Modal.19ffed32.js | 41.27kb | +0.00b | 6.18s | -568.00ms 🚀 |
| dist/component.d7c29980.js | 30.70kb | +0.00b | 7.17s | +426.00ms ⚠️ |
| dist/component.54080a26.js | 22.50kb | +0.00b | 6.18s | -550.00ms 🚀 |
| dist/dropzone.291ad844.js | 17.34kb | +0.00b | 1.05m | -46.28s 🚀 |
| dist/js.5733a017.js | 16.40kb | +0.00b | 6.18s | -550.00ms 🚀 |
| dist/ui.fee77548.js | 13.97kb | +0.00b | 15.86s | +1.39s ⚠️ |
| dist/component.77c0f682.js | 6.05kb | +0.00b | 6.77s | -2.66s 🚀 |
| dist/EmojiPickerComponent.627aca6b.js | 3.58kb | +0.00b | 12.86s | +1.75s ⚠️ |
| dist/index.ac562c80.css | 3.46kb | +0.00b | 1.32m | +11.60s ⚠️ |
| dist/png-chunks-extract.a844fdb6.js | 3.41kb | +0.00b | 6.18s | -550.00ms 🚀 |
| dist/Modal.0304b9ed.js | 3.08kb | +0.00b | 6.18s | -550.00ms 🚀 |
| dist/16.290b4a43.js | 2.38kb | +0.00b | 1.88s | -429.00ms 🚀 |
| dist/ResourcedEmojiComponent.abe8596c.js | 2.00kb | +0.00b | 7.13s | +434.00ms ⚠️ |
| dist/workerHasher.6d5b22d7.js | 1.54kb | +0.00b | 15.20s | -1.56m 🚀 |
| dist/workerHasher.fa198233.js | 1.54kb | +0.00b | 1.82m | +44.68s ⚠️ |
| dist/16.9ccb5b28.js | 1.38kb | +0.00b | 7.01s | -2.51s 🚀 |
| dist/16.51049e2c.js | 1.32kb | +0.00b | 1.88s | -429.00ms 🚀 |
| dist/16.18344f47.js | 1.31kb | +0.00b | 11.41s | +3.84s ⚠️ |
| dist/16.436f097f.js | 1.21kb | +0.00b | 1.88s | -429.00ms 🚀 |
| dist/16.02c68c4f.js | 1.18kb | +0.00b | 11.41s | +3.84s ⚠️ |
| dist/16.b46ef690.js | 1.14kb | +0.00b | 1.88s | -429.00ms 🚀 |
| dist/16.4e3b0235.js | 1.14kb | +0.00b | 7.01s | -357.00ms 🚀 |
| dist/16.9adb4ec7.js | 1.13kb | +0.00b | 7.01s | -358.00ms 🚀 |
| dist/16.eafe1a11.js | 1.13kb | +0.00b | 6.77s | -2.66s 🚀 |
| dist/16.3920766f.js | 1.10kb | +0.00b | 1.88s | -429.00ms 🚀 |
| dist/component.6600f83e.js | 1.07kb | +0.00b | 7.13s | +434.00ms ⚠️ |
| dist/16.afcb09b6.js | 1.05kb | +0.00b | 7.21s | -362.00ms 🚀 |
| dist/action.9aa72b39.js | 1.00kb | +0.00b | 11.57s | +3.99s ⚠️ |
| dist/media-card-analytics-error-boundary.663369b9.js | 835.00b | +0.00b | 1.81m | +1.56m ⚠️ |
| dist/index.html | 119.00b | +0.00b | 9.23s | +780.00ms ⚠️ |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.accd8b81.js | 2.72mb | +3.00b ⚠️ | 461.00ms | +155.00ms ⚠️ |
| dist/pdfRenderer.15a6a023.js | 1.11mb | +0.00b | 649.00ms | +47.00ms ⚠️ |
| dist/editorView.e06544fb.js | 587.12kb | +0.00b | 676.00ms | +50.00ms ⚠️ |
| dist/popup.6c453e3c.js | 169.96kb | +0.00b | 676.00ms | +61.00ms ⚠️ |
| dist/Toolbar.83ac0b6e.js | 92.34kb | +0.00b | 676.00ms | +50.00ms ⚠️ |
| dist/EmojiPickerComponent.ae601247.js | 75.29kb | +0.00b | 628.00ms | +90.00ms ⚠️ |
| dist/media-viewer.7079364e.js | 72.03kb | +0.00b | 643.00ms | +53.00ms ⚠️ |
| dist/card.645a5c17.js | 55.01kb | +0.00b | 643.00ms | +53.00ms ⚠️ |
| dist/card.0313bac8.js | 52.78kb | +0.00b | 669.00ms | +48.00ms ⚠️ |
| dist/Modal.19ffed32.js | 41.27kb | +0.00b | 468.00ms | +147.00ms ⚠️ |
| dist/component.d7c29980.js | 30.70kb | +0.00b | 461.00ms | +155.00ms ⚠️ |
| dist/component.54080a26.js | 22.50kb | +0.00b | 468.00ms | +118.00ms ⚠️ |
| dist/DatePicker.0df05dac.js | 21.21kb | +0.00b | 627.00ms | +89.00ms ⚠️ |
| dist/EmojiPickerComponent.7dc0ebd0.js | 18.23kb | +0.00b | 628.00ms | +90.00ms ⚠️ |
| dist/dropzone.291ad844.js | 17.34kb | +0.00b | 669.00ms | +48.00ms ⚠️ |
| dist/smartMediaEditor.125a19a7.js | 16.93kb | +0.00b | 676.00ms | +50.00ms ⚠️ |
| dist/js.5733a017.js | 16.40kb | +0.00b | 468.00ms | +147.00ms ⚠️ |
| dist/ui.fee77548.js | 13.97kb | +0.00b | 643.00ms | +53.00ms ⚠️ |
| dist/workerHasher.1e706e77.js | 11.75kb | +0.00b | 644.00ms | +70.00ms ⚠️ |
| dist/EmojiTypeAheadComponent.1990d771.js | 8.67kb | +0.00b | 643.00ms | +69.00ms ⚠️ |
| dist/component.77c0f682.js | 6.05kb | +0.00b | 485.00ms | +135.00ms ⚠️ |
| dist/card.383bd9bd.js | 5.61kb | +0.00b | 643.00ms | +53.00ms ⚠️ |
| dist/media-viewer.59c17dad.js | 3.92kb | +0.00b | 649.00ms | +47.00ms ⚠️ |
| dist/EmojiPickerComponent.627aca6b.js | 3.58kb | +0.00b | 628.00ms | +90.00ms ⚠️ |
| dist/png-chunks-extract.a844fdb6.js | 3.41kb | +0.00b | 468.00ms | +147.00ms ⚠️ |
| dist/dropzone.3c67f0a6.js | 3.23kb | +0.00b | 676.00ms | +60.00ms ⚠️ |
| dist/Modal.0304b9ed.js | 3.08kb | +0.00b | 468.00ms | +147.00ms ⚠️ |
| dist/clipboard.da112b6e.js | 2.75kb | +0.00b | 676.00ms | +60.00ms ⚠️ |
| dist/16.290b4a43.js | 2.38kb | +0.00b | 614.00ms | +294.00ms ⚠️ |
| dist/EmojiUploadComponent.d54a1fc6.js | 2.18kb | +0.00b | 643.00ms | +69.00ms ⚠️ |
| dist/card.eea6ad8c.js | 2.03kb | +0.00b | 649.00ms | +47.00ms ⚠️ |
| dist/ResourcedEmojiComponent.abe8596c.js | 2.00kb | +0.00b | 461.00ms | +156.00ms ⚠️ |
| dist/date.231500d2.js | 1.73kb | +0.00b | 515.00ms | +82.00ms ⚠️ |
| dist/16.d16f14ed.js | 1.73kb | +0.00b | 507.00ms | +115.00ms ⚠️ |
| dist/images.80c92f5e.js | 1.67kb | +0.00b | 520.00ms | +86.00ms ⚠️ |
| dist/16.1b6ca79f.js | 1.66kb | +0.00b | 485.00ms | +134.00ms ⚠️ |
| dist/feedback.d5590b34.js | 1.64kb | +0.00b | 611.00ms | +96.00ms ⚠️ |
| dist/browser.75813db0.js | 1.61kb | +0.00b | 676.00ms | +60.00ms ⚠️ |
| dist/workerHasher.6d5b22d7.js | 1.54kb | +0.00b | 644.00ms | +70.00ms ⚠️ |
| dist/workerHasher.fa198233.js | 1.54kb | +0.00b | 669.00ms | +47.00ms ⚠️ |
| dist/list-number.c35f9589.js | 1.46kb | +0.00b | 552.00ms | +97.00ms ⚠️ |
| dist/status.8ab4226b.js | 1.46kb | +0.00b | 591.00ms | +101.00ms ⚠️ |
| dist/16.9ccb5b28.js | 1.38kb | +0.00b | 498.00ms | +118.00ms ⚠️ |
| dist/code.3dd71280.js | 1.38kb | +0.00b | 515.00ms | +111.00ms ⚠️ |
| dist/16.19c93285.js | 1.33kb | +0.00b | 485.00ms | +135.00ms ⚠️ |
| dist/16.c7751e34.js | 1.33kb | +0.00b | 498.00ms | +119.00ms ⚠️ |
| dist/16.51049e2c.js | 1.32kb | +0.00b | 614.00ms | -77.00ms 🚀 |
| dist/16.18344f47.js | 1.31kb | +0.00b | 514.00ms | +110.00ms ⚠️ |
| dist/link.2f4427e6.js | 1.30kb | +0.00b | 552.00ms | +97.00ms ⚠️ |
| dist/heading6.fdbbdf4d.js | 1.30kb | +0.00b | 611.00ms | +96.00ms ⚠️ |
| dist/heading3.2113b7a1.js | 1.29kb | +0.00b | 593.00ms | +102.00ms ⚠️ |
| dist/16.2c4c1846.js | 1.27kb | +0.00b | 508.00ms | +104.00ms ⚠️ |
| dist/emoji.93ae61d3.js | 1.23kb | +0.00b | 520.00ms | +86.00ms ⚠️ |
| dist/16.d17ab186.js | 1.23kb | +0.00b | 508.00ms | +115.00ms ⚠️ |
| dist/16.436f097f.js | 1.21kb | +0.00b | 614.00ms | -77.00ms 🚀 |
| dist/16.b4da0acb.js | 1.19kb | +0.00b | 508.00ms | +115.00ms ⚠️ |
| dist/16.02c68c4f.js | 1.18kb | +0.00b | 515.00ms | +111.00ms ⚠️ |
| dist/heading5.e195aab1.js | 1.17kb | +0.00b | 611.00ms | +96.00ms ⚠️ |
| dist/expand.450f3aea.js | 1.16kb | +0.00b | 612.00ms | +97.00ms ⚠️ |
| dist/media-card-analytics-error-boundary.2323af57.js | 1.15kb | +0.00b | 649.00ms | +47.00ms ⚠️ |
| dist/16.b46ef690.js | 1.14kb | +0.00b | 614.00ms | -76.00ms 🚀 |
| dist/16.4e3b0235.js | 1.14kb | +0.00b | 498.00ms | +106.00ms ⚠️ |
| dist/16.9ba230ea.js | 1.13kb | +0.00b | 485.00ms | +107.00ms ⚠️ |
| dist/16.a37df53b.js | 1.13kb | +0.00b | 498.00ms | +119.00ms ⚠️ |
| dist/16.9adb4ec7.js | 1.13kb | +0.00b | 498.00ms | +118.00ms ⚠️ |
| dist/16.eafe1a11.js | 1.13kb | +0.00b | 485.00ms | +135.00ms ⚠️ |
| dist/heading2.a2a1be3f.js | 1.11kb | +0.00b | 593.00ms | +102.00ms ⚠️ |
| dist/16.3920766f.js | 1.10kb | +0.00b | 614.00ms | -76.00ms 🚀 |
| dist/component.6600f83e.js | 1.07kb | +0.00b | 461.00ms | +155.00ms ⚠️ |
| dist/mention.4d64e059.js | 1.07kb | +0.00b | 552.00ms | +97.00ms ⚠️ |
| dist/heading4.ec980e93.js | 1.06kb | +0.00b | 611.00ms | +97.00ms ⚠️ |
| dist/16.afcb09b6.js | 1.05kb | +0.00b | 508.00ms | +115.00ms ⚠️ |
| dist/layout.52ba15f7.js | 1.05kb | +0.00b | 520.00ms | -171.00ms 🚀 |
| dist/divider.16defde6.js | 1.03kb | +0.00b | 520.00ms | +86.00ms ⚠️ |
| dist/quote.aa25b83c.js | 1.02kb | +0.00b | 580.00ms | +105.00ms ⚠️ |
| dist/action.9aa72b39.js | 1.00kb | +0.00b | 515.00ms | +111.00ms ⚠️ |
| dist/decision.20244046.js | 1010.00b | +0.00b | 520.00ms | +87.00ms ⚠️ |
| dist/panel-warning.97a0e3b3.js | 1008.00b | +0.00b | 579.00ms | +104.00ms ⚠️ |
| dist/list.c9147a62.js | 978.00b | +0.00b | 552.00ms | +97.00ms ⚠️ |
| dist/heading1.7d899ba5.js | 976.00b | +0.00b | 593.00ms | +102.00ms ⚠️ |
| dist/panel-error.8d11e8b7.js | 906.00b | +0.00b | 552.00ms | +97.00ms ⚠️ |
| dist/panel.a28c56ee.js | 897.00b | +0.00b | 580.00ms | +105.00ms ⚠️ |
| dist/table.5a4874ee.js | 889.00b | +0.00b | 592.00ms | +101.00ms ⚠️ |
| dist/panel-success.9b923e58.js | 845.00b | +0.00b | 579.00ms | +104.00ms ⚠️ |
| dist/panel-note.97a4d541.js | 841.00b | +0.00b | 579.00ms | +105.00ms ⚠️ |
| dist/media-picker-analytics-error-boundary.f8ddfee3.js | 839.00b | +0.00b | 676.00ms | +60.00ms ⚠️ |
| dist/media-card-analytics-error-boundary.663369b9.js | 835.00b | +0.00b | 643.00ms | +53.00ms ⚠️ |
| dist/media-card-analytics-error-boundary.73ba4074.js | 835.00b | +0.00b | 669.00ms | +48.00ms ⚠️ |
| dist/media-viewer-analytics-error-boundary.0afa7031.js | 755.00b | +0.00b | 649.00ms | +47.00ms ⚠️ |
| dist/EmojiPickerComponent.2e54b9c6.js | 535.00b | +0.00b | 628.00ms | +89.00ms ⚠️ |
| dist/simpleHasher.16c8bf41.js | 528.00b | +0.00b | 644.00ms | +69.00ms ⚠️ |
| dist/simpleHasher.a29b2a8e.js | 528.00b | +0.00b | 669.00ms | +47.00ms ⚠️ |
| dist/index.html | 119.00b | +0.00b | 461.00ms | +155.00ms ⚠️ |

</p></details>

<details><summary>Three.js x4 🚨</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | **FAILED** | -0.00ms |
| Cached | **FAILED** | -0.00ms |

#### Cold Bundles

*No bundles found, this is probably a failed build...*
#### Cached Bundles

*No bundles found, this is probably a failed build...*
</p></details>

**[Click here to view a detailed benchmark overview.](https://benchmark.parceljs.org/benchmark/279511868194685448)**

Patch:
diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js
index d55258602..77f957561 100644
--- a/packages/core/core/src/Parcel.js
+++ b/packages/core/core/src/Parcel.js
@@ -39,6 +39,7 @@ import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill';
 import {PromiseQueue} from '@parcel/utils';
 import ParcelConfig from './ParcelConfig';
 import logger from '@parcel/logger';
+import {Disposable} from '@parcel/events';
 
 registerCoreWithSerializer();
 
@@ -53,6 +54,7 @@ export default class Parcel {
   #config /*: ParcelConfig*/;
   #farm /*: WorkerFarm*/;
   #initialized /*: boolean*/ = false;
+  #disposable /*: Disposable */;
   #initialOptions /*: InitialParcelOptions*/;
   #reporterRunner /*: ReporterRunner*/;
   #resolvedOptions /*: ?ParcelOptions*/ = null;
@@ -69,16 +71,7 @@ export default class Parcel {
         +buildEvent: BuildEvent,
         +error?: void,
       |},
-  > */ = new ValueEmitter<
-    | {|
-        +error: Error,
-        +buildEvent?: void,
-      |}
-    | {|
-        +buildEvent: BuildEvent,
-        +error?: void,
-      |},
-  >();
+  > */;
   #watcherSubscription /*: ?AsyncSubscription*/;
   #watcherCount /*: number*/ = 0;
 
@@ -88,7 +81,7 @@ export default class Parcel {
     this.#initialOptions = options;
   }
 
-  async init(): Promise<void> {
+  async _init(): Promise<void> {
     if (this.#initialized) {
       return;
     }
@@ -105,18 +98,39 @@ export default class Parcel {
       resolvedOptions.inputFS,
       resolvedOptions.autoinstall,
     );
-    this.#farm =
-      this.#initialOptions.workerFarm ??
-      createWorkerFarm({
+
+    if (this.#initialOptions.workerFarm) {
+      if (this.#initialOptions.workerFarm.ending) {
+        throw new Error('Supplied WorkerFarm is ending');
+      }
+      this.#farm = this.#initialOptions.workerFarm;
+    } else {
+      this.#farm = createWorkerFarm({
         patchConsole: resolvedOptions.patchConsole,
       });
+    }
 
-    // ? Should we have a dispose function on the Parcel class or should we create these references
-    //  - in run and watch and dispose at the end of run and in the unsubsribe function of watch
-    let {ref: optionsRef} = await this.#farm.createSharedReference(
-      resolvedOptions,
-    );
-    let {ref: configRef} = await this.#farm.createSharedReference(config);
+    let {
+      dispose: disposeOptions,
+      ref: optionsRef,
+    } = await this.#farm.createSharedReference(resolvedOptions);
+    let {
+      dispose: disposeConfig,
+      ref: configRef,
+    } = await this.#farm.createSharedReference(config);
+
+    this.#disposable = new Disposable();
+    if (this.#initialOptions.workerFarm) {
+      // If we don't own the farm, dispose of only these references when
+      // Parcel ends.
+      this.#disposable.add(disposeOptions, disposeConfig);
+    } else {
+      // Otherwise, when shutting down, end the entire farm we created.
+      this.#disposable.add(() => this.#farm.end());
+    }
+
+    this.#watchEvents = new ValueEmitter();
+    this.#disposable.add(() => this.#watchEvents.dispose());
 
     this.#assetGraphBuilder = new AssetGraphBuilder();
     this.#runtimesAssetGraphBuilder = new AssetGraphBuilder();
@@ -165,19 +179,11 @@ export default class Parcel {
   async run(): Promise<BuildSuccessEvent> {
     let startTime = Date.now();
     if (!this.#initialized) {
-      await this.init();
+      await this._init();
     }
 
-    let result = await this.build({startTime});
-    await Promise.all([
-      this.#assetGraphBuilder.writeToCache(),
-      this.#runtimesAssetGraphBuilder.writeToCache(),
-    ]);
-
-    if (!this.#initialOptions.workerFarm) {
-      // If there wasn't a workerFarm passed in, we created it. End the farm.
-      await this.#farm.end();
-    }
+    let result = await this._build({startTime});
+    await this._end();
 
     if (result.type === 'buildFailure') {
       throw new BuildError(result.diagnostics);
@@ -186,12 +192,22 @@ export default class Parcel {
     return result;
   }
 
-  async startNextBuild() {
+  async _end(): Promise<void> {
+    this.#initialized = false;
+
+    await Promise.all([
+      this.#disposable.dispose(),
+      this.#assetGraphBuilder.writeToCache(),
+      this.#runtimesAssetGraphBuilder.writeToCache(),
+    ]);
+  }
+
+  async _startNextBuild() {
     this.#watchAbortController = new AbortController();
 
     try {
       this.#watchEvents.emit({
-        buildEvent: await this.build({
+        buildEvent: await this._build({
           signal: this.#watchAbortController.signal,
         }),
       });
@@ -206,6 +222,10 @@ export default class Parcel {
   async watch(
     cb?: (err: ?Error, buildEvent?: BuildEvent) => mixed,
   ): Promise<AsyncSubscription> {
+    if (!this.#initialized) {
+      await this._init();
+    }
+
     let watchEventsDisposable;
     if (cb) {
       watchEventsDisposable = this.#watchEvents.addListener(
@@ -214,16 +234,12 @@ export default class Parcel {
     }
 
     if (this.#watcherCount === 0) {
-      if (!this.#initialized) {
-        await this.init();
-      }
-
       this.#watcherSubscription = await this._getWatcherSubscription();
       await this.#reporterRunner.report({type: 'watchStart'});
 
       // Kick off a first build, but don't await its results. Its results will
       // be provided to the callback.
-      this.#watchQueue.add(() => this.startNextBuild());
+      this.#watchQueue.add(() => this._startNextBuild());
       this.#watchQueue.run();
     }
 
@@ -240,10 +256,9 @@ export default class Parcel {
         await nullthrows(this.#watcherSubscription).unsubscribe();
         this.#watcherSubscription = null;
         await this.#reporterRunner.report({type: 'watchEnd'});
-        await Promise.all([
-          this.#assetGraphBuilder.writeToCache(),
-          this.#runtimesAssetGraphBuilder.writeToCache(),
-        ]);
+        this.#watchAbortController.abort();
+        await this.#watchQueue.run();
+        await this._end();
       }
     };
 
@@ -258,13 +273,13 @@ export default class Parcel {
     };
   }
 
-  async build({
+  async _build({
     signal,
     startTime = Date.now(),
   }: {|
     signal?: AbortSignal,
     startTime?: number,
-  |}): Promise<BuildEvent> {
+  |} = {}): Promise<BuildEvent> {
     let options = nullthrows(this.#resolvedOptions);
     try {
       if (options.profile) {
@@ -299,7 +314,6 @@ export default class Parcel {
       };
 
       await this.#reporterRunner.report(event);
-
       await this.#assetGraphBuilder.validate();
       return event;
     } catch (e) {
@@ -391,7 +405,7 @@ export default class Parcel {
             this.#watchAbortController.abort();
           }
 
-          this.#watchQueue.add(() => this.startNextBuild());
+          this.#watchQueue.add(() => this._startNextBuild());
           this.#watchQueue.run();
         }
       },
diff --git a/packages/core/register/src/register.js b/packages/core/register/src/register.js
index e856fd926..3ea1d0e60 100644
--- a/packages/core/register/src/register.js
+++ b/packages/core/register/src/register.js
@@ -47,7 +47,7 @@ function register(inputOpts?: InitialParcelOptions): IDisposable {
     },
   };
 
-  syncPromise(parcel.init());
+  syncPromise(parcel._init());
 
   let isProcessing = false;
 
diff --git a/packages/core/workers/src/WorkerFarm.js b/packages/core/workers/src/WorkerFarm.js
index 715be8a61..1e898a222 100644
--- a/packages/core/workers/src/WorkerFarm.js
+++ b/packages/core/workers/src/WorkerFarm.js
@@ -351,6 +351,10 @@ export default class WorkerFarm extends EventEmitter {
   async end(): Promise<void> {
     this.ending = true;
 
+    await Promise.all(
+      Array.from(this.workers.values()).map(worker => this.stopWorker(worker)),
+    );
+
     for (let handle of this.handles.values()) {
       handle.dispose();
     }
@@ -358,9 +362,6 @@ export default class WorkerFarm extends EventEmitter {
     this.sharedReferences = new Map();
     this.sharedReferencesByValue = new Map();
 
-    await Promise.all(
-      Array.from(this.workers.values()).map(worker => this.stopWorker(worker)),
-    );
     this.ending = false;
   }
 
diff --git a/packages/utils/events/src/Disposable.js b/packages/utils/events/src/Disposable.js
index 2c55b0ae3..6f4ad9cd5 100644
--- a/packages/utils/events/src/Disposable.js
+++ b/packages/utils/events/src/Disposable.js
@@ -33,21 +33,20 @@ export default class Disposable implements IDisposable {
     }
   }
 
-  dispose(): void {
+  async dispose(): Promise<void> {
     if (this.disposed) {
       return;
     }
 
+    this.disposed = true;
+
     invariant(this.#disposables != null);
-    for (let disposable of this.#disposables) {
-      if (typeof disposable === 'function') {
-        disposable();
-      } else {
-        disposable.dispose();
-      }
-    }
+    await Promise.all(
+      [...this.#disposables].map(disposable =>
+        typeof disposable === 'function' ? disposable() : disposable.dispose(),
+      ),
+    );
 
     this.#disposables = null;
-    this.disposed = true;
   }
 }
diff --git a/packages/utils/events/src/types.js b/packages/utils/events/src/types.js
index 02ea794ae..438e9c472 100644
--- a/packages/utils/events/src/types.js
+++ b/packages/utils/events/src/types.js
@@ -1,5 +1,7 @@
 // @flow strict-local
 
 export interface IDisposable {
-  dispose(): void;
+  // This can return a Promise, as dispose() of all inner disposables are
+  // awaited in Disposable#dispose()
+  dispose(): mixed;
 }
diff --git a/yarn.lock b/yarn.lock
index 5c8fbb6ae..6a22bf146 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1980,13 +1980,6 @@
   dependencies:
     type-detect "4.0.8"
 
-"@sinonjs/formatio@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
-  integrity sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==
-  dependencies:
-    samsam "1.3.0"
-
 "@sinonjs/formatio@^3.2.1":
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c"
@@ -8891,11 +8884,6 @@ log-update@^4.0.0:
     slice-ansi "^4.0.0"
     wrap-ansi "^6.2.0"
 
-lolex@^2.4.2:
-  version "2.7.5"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733"
-  integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==
-
 lolex@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7"
@@ -9608,7 +9596,7 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
-nise@^1.3.3, nise@^1.5.2:
+nise@^1.5.2:
   version "1.5.3"
   resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7"
   integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==
@@ -12312,11 +12300,6 @@ safe-regex@^1.1.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-samsam@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
-  integrity sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==
-
 sass@^1.10.2, sass@^1.22.9:
   version "1.25.0"
   resolved "https://registry.yarnpkg.com/sass/-/sass-1.25.0.tgz#f8bd7dfbb39d6b0305e27704a8ebe637820693f3"
@@ -12544,19 +12527,6 @@ simple-swizzle@^0.2.2:
   dependencies:
     is-arrayish "^0.3.1"
 
-sinon@^5.0.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-5.1.1.tgz#19c59810ffb733ea6e76a28b94a71fc4c2f523b8"
-  integrity sha512-h/3uHscbt5pQNxkf7Y/Lb9/OM44YNCicHakcq73ncbrIS8lXg+ZGOZbtuU+/km4YnyiCYfQQEwANaReJz7KDfw==
-  dependencies:
-    "@sinonjs/formatio" "^2.0.0"
-    diff "^3.5.0"
-    lodash.get "^4.4.2"
-    lolex "^2.4.2"
-    nise "^1.3.3"
-    supports-color "^5.4.0"
-    type-detect "^4.0.8"
-
 sinon@^7.3.1:
   version "7.5.0"
   resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec"
@@ -13693,7 +13663,7 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-detect@4.0.8, type-detect@^4.0.8:
+type-detect@4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==


Test patch:
diff --git a/packages/core/core/test/Parcel.test.js b/packages/core/core/test/Parcel.test.js
new file mode 100644
index 000000000..2e65c23da
--- /dev/null
+++ b/packages/core/core/test/Parcel.test.js
@@ -0,0 +1,88 @@
+// @flow strict-local
+
+import type {InitialParcelOptions} from '@parcel/types';
+import WorkerFarm from '@parcel/workers';
+// flowlint-next-line untyped-import:off
+import sinon from 'sinon';
+import assert from 'assert';
+import path from 'path';
+import Parcel, {createWorkerFarm} from '../src/Parcel';
+
+describe('Parcel', function() {
+  this.timeout(40000);
+
+  let workerFarm;
+  before(() => {
+    workerFarm = createWorkerFarm();
+  });
+
+  after(() => workerFarm.end());
+
+  it('does not initialize when passed an ending farm', async () => {
+    workerFarm.ending = true;
+    let parcel = createParcel({workerFarm});
+
+    // $FlowFixMe
+    await assert.rejects(() => parcel.run(), {
+      name: 'Error',
+      message: 'Supplied WorkerFarm is ending',
+    });
+
+    workerFarm.ending = false;
+  });
+
+  describe('parcel.end()', () => {
+    let endSpy;
+    beforeEach(() => {
+      endSpy = sinon.spy(WorkerFarm.prototype, 'end');
+    });
+
+    afterEach(() => {
+      endSpy.restore();
+    });
+
+    it('ends any WorkerFarm it creates', async () => {
+      let parcel = createParcel();
+      await parcel.run();
+      assert.equal(endSpy.callCount, 1);
+    });
+
+    it('runs and constructs another farm for subsequent builds', async () => {
+      let parcel = createParcel();
+
+      await parcel.run();
+      await parcel.run();
+
+      assert.equal(endSpy.callCount, 2);
+    });
+
+    it('does not end passed WorkerFarms', async () => {
+      let parcel = createParcel({workerFarm});
+      await parcel.run();
+      assert.equal(endSpy.callCount, 0);
+
+      await workerFarm.end();
+    });
+
+    it('removes shared references it creates', async () => {
+      let parcel = createParcel({workerFarm});
+      await parcel.run();
+
+      assert.equal(workerFarm.sharedReferences.size, 0);
+      assert.equal(workerFarm.sharedReferencesByValue.size, 0);
+      await workerFarm.end();
+    });
+  });
+});
+
+function createParcel(opts?: InitialParcelOptions) {
+  return new Parcel({
+    entries: [path.join(__dirname, 'fixtures/parcel/index.js')],
+    logLevel: 'info',
+    defaultConfig: path.join(
+      path.dirname(require.resolve('@parcel/test-utils')),
+      '.parcelrc-no-reporters',
+    ),
+    ...opts,
+  });
+}
diff --git a/packages/core/core/test/fixtures/parcel/index.js b/packages/core/core/test/fixtures/parcel/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/core/core/test/fixtures/parcel/package.json b/packages/core/core/test/fixtures/parcel/package.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/packages/core/core/test/fixtures/parcel/package.json
@@ -0,0 +1,1 @@
+{}
diff --git a/packages/core/core/test/fixtures/parcel/yarn.lock b/packages/core/core/test/fixtures/parcel/yarn.lock
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/core/integration-tests/package.json b/packages/core/integration-tests/package.json
index 3836a7c9a..82999fb83 100644
--- a/packages/core/integration-tests/package.json
+++ b/packages/core/integration-tests/package.json
@@ -42,7 +42,6 @@
     "react-dom": "^16.11.0",
     "react": "^16.11.0",
     "rimraf": "^2.6.1",
-    "sinon": "^5.0.1",
     "tempy": "^0.3.0",
     "ws": "^5.1.1"
   }
diff --git a/packages/core/test-utils/package.json b/packages/core/test-utils/package.json
index af59c7883..cafdc5ce5 100644
--- a/packages/core/test-utils/package.json
+++ b/packages/core/test-utils/package.json
@@ -8,8 +8,7 @@
     "type": "git",
     "url": "https://github.com/parcel-bundler/parcel.git"
   },
-  "main": "lib/utils.js",
-  "source": "src/utils.js",
+  "main": "src/utils.js",
   "engines": {
     "node": ">= 10.0.0"
   },