Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: print location of unsettled top-level await in entry points #51999

Closed
wants to merge 5 commits into from

Conversation

joyeecheung
Copy link
Member

@joyeecheung joyeecheung commented Mar 7, 2024

module: refactor ESM loader initialization and entry point handling

Split the internal/process/esm_loader file which contains the
singleton cascaded loader:

  • The the singleton cascaded loader now directly resides in
    internal/modules/esm/loader, where the constructor also lives.
    This file is the root of most circular dependency of ESM code,
    (because components of the loader need the singleton itself),
    so this makes the dependency more obvious. Added comments about
    loading it lazily to avoid circular dependency.
  • The getter to the cascaded loader is also turned into a method
    to make the side effect explicit.
  • The sequence of loadESM() and handleMainPromise is now merged
    together into runEntryPointWithESMLoader() in
    internal/modules/run_main because this is intended to run entry
    points with the ESM loader and not just any module.
  • Documents how top-level await is handled.

src: refactor out FormatErrorMessage for error formatting

module: print location of unsettled top-level await in entry points

When the entry point is a module and the graph it imports still
contains unsettled top-level await when the Node.js instance
finishes the event loop, search from the entry point module
for unsettled top-level await and print their location.

To avoid unnecessary overhead, we register a promise that only
gets settled when the entry point graph evaluation returns
from await, and only search the module graph if it's still
unsettled by the time the instance is exiting.

This patch only handles this when the root module of the graph is
also the entry point of the Node.js instance. Other kinds of root
modules are more complicated so will be left for the future.

Drive-by: update the terminology "unfinished promise" to the
more correct one "unsettled promise" in the codebase.

Fixes: #42868

Split the `internal/process/esm_loader` file which contains the
singleton cascaded loader:

- The the singleton cascaded loader now directly resides in
  `internal/modules/esm/loader`, where the constructor also lives.
  This file is the root of most circular dependency of ESM code,
  (because components of the loader need the singleton itself),
  so this makes the dependency more obvious. Added comments about
  loading it lazily to avoid circular dependency.
- The getter to the cascaded loader is also turned into a method
  to make the side effect explicit.
- The sequence of `loadESM()` and `handleMainPromise` is now merged
  together into `runEntryPointWithESMLoader()` in
  `internal/modules/run_main` because this is intended to run entry
  points with the ESM loader and not just any module.
- Documents how top-level await is handled.
When the entry point is a module and the graph it imports still
contains unsettled top-level await when the Node.js instance
finishes the event loop, search from the entry point module
for unsettled top-level await and print their location.

To avoid unnecessary overhead, we register a promise that only
gets settled when the entry point graph evaluation returns
from await, and only search the module graph if it's still
unsettled by the time the instance is exiting.

This patch only handles this for entry point modules. Other kinds of
modules are more complicated so will be left for the future.

Drive-by: update the terminology "unfinished promise" to the
more correct one "unsettled promise" in the codebase.
@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/test_runner
  • @nodejs/vm

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Mar 7, 2024
@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 7, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 7, 2024
@nodejs-github-bot
Copy link
Collaborator

Copy link
Member

@MoLow MoLow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS changes LGTM

@targos
Copy link
Member

targos commented Mar 7, 2024

Codeowners lint:

==> Executing File Exist Checker (213.485787ms)
[err] line 99: "/lib/internal/process/esm_loader.js" does not match any files in repository

lib/internal/modules/esm/module_job.js Outdated Show resolved Hide resolved
lib/internal/modules/run_main.js Outdated Show resolved Hide resolved
CHECK_EQ(object->InternalFieldCount(),
loader::ModuleWrap::kInternalFieldCount);
auto* wrap = BaseObject::FromJSObject<loader::ModuleWrap>(object);
return wrap->CheckUnsettledTopLevelAwait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an assert here that we expect wrap->CheckUnsettledTopLevelAwait() to not return v8::Just(true)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can return true, for example if it's not a source text module (and therefore it's not caused by TLA per-se), or any other situations where V8 is not able to find it. This is only to help debugging anyway, crashing here is not going to help anyone.

Copy link
Contributor

@aduh95 aduh95 Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought would could get into this branch only if there was some unsettled TLA (because of the various if above), so CheckUnsettledTopLevelAwait would return false only if there was a bug somewhere

This is only to help debugging anyway, crashing here is not going to help anyone.

I guess it depends if getting report would help us improve the detection or not. If you think it would not, I agree we don't need the assert

@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 7, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 7, 2024
@nodejs-github-bot
Copy link
Collaborator

@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 7, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 7, 2024
@nodejs-github-bot
Copy link
Collaborator

@joyeecheung joyeecheung added commit-queue Add this label to land a pull request using GitHub Actions. and removed commit-queue Add this label to land a pull request using GitHub Actions. labels Mar 8, 2024
source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href,
) {
async eval(source, url, isEntryPoint = false) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this method lacks a JSDoc, do you mind adding one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, didn't see this, feel free to follow up and add a comment if you like.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird! I just looked through the history, and it seems this method never got a jsdoc when pretty much everything else did.

joyeecheung added a commit that referenced this pull request Mar 10, 2024
Split the `internal/process/esm_loader` file which contains the
singleton cascaded loader:

- The the singleton cascaded loader now directly resides in
  `internal/modules/esm/loader`, where the constructor also lives.
  This file is the root of most circular dependency of ESM code,
  (because components of the loader need the singleton itself),
  so this makes the dependency more obvious. Added comments about
  loading it lazily to avoid circular dependency.
- The getter to the cascaded loader is also turned into a method
  to make the side effect explicit.
- The sequence of `loadESM()` and `handleMainPromise` is now merged
  together into `runEntryPointWithESMLoader()` in
  `internal/modules/run_main` because this is intended to run entry
  points with the ESM loader and not just any module.
- Documents how top-level await is handled.

PR-URL: #51999
Fixes: #42868
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
joyeecheung added a commit that referenced this pull request Mar 10, 2024
PR-URL: #51999
Fixes: #42868
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
joyeecheung added a commit that referenced this pull request Mar 10, 2024
When the entry point is a module and the graph it imports still
contains unsettled top-level await when the Node.js instance
finishes the event loop, search from the entry point module
for unsettled top-level await and print their location.

To avoid unnecessary overhead, we register a promise that only
gets settled when the entry point graph evaluation returns
from await, and only search the module graph if it's still
unsettled by the time the instance is exiting.

This patch only handles this for entry point modules. Other kinds of
modules are more complicated so will be left for the future.

Drive-by: update the terminology "unfinished promise" to the
more correct one "unsettled promise" in the codebase.

PR-URL: #51999
Fixes: #42868
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
@joyeecheung
Copy link
Member Author

Landed in 257f322...575ced8

Copy link
Contributor

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huzzah! Thanks for this @joyeecheung


/**
* This is a singleton ESM loader that integrates the loader hooks, if any.
* It it used by other internal built-ins when they need to load ESM code
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* It it used by other internal built-ins when they need to load ESM code
* It is used by other internal built-ins when they need to load ESM code

* it finishes evaluation.
* @param {string} source Source code the ESM
* @param {boolean} print Whether the result should be printed.
* @returns {Promise}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what does the promise resolve to?

@@ -568,6 +561,23 @@ function getHooksProxy() {
return hooksProxy;
}

let cascadedLoader;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: why call it "cascaded" loader?

source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href,
) {
async eval(source, url, isEntryPoint = false) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird! I just looked through the history, and it seems this method never got a jsdoc when pretty much everything else did.

@aduh95 aduh95 added the baking-for-lts PRs that need to wait before landing in a LTS release. label Mar 10, 2024
@aduh95
Copy link
Contributor

aduh95 commented Mar 10, 2024

Marking it as baking-for-lts PRs that need to wait before landing in a LTS release. in case the added output breaks someone.

rdw-msft pushed a commit to rdw-msft/node that referenced this pull request Mar 26, 2024
Split the `internal/process/esm_loader` file which contains the
singleton cascaded loader:

- The the singleton cascaded loader now directly resides in
  `internal/modules/esm/loader`, where the constructor also lives.
  This file is the root of most circular dependency of ESM code,
  (because components of the loader need the singleton itself),
  so this makes the dependency more obvious. Added comments about
  loading it lazily to avoid circular dependency.
- The getter to the cascaded loader is also turned into a method
  to make the side effect explicit.
- The sequence of `loadESM()` and `handleMainPromise` is now merged
  together into `runEntryPointWithESMLoader()` in
  `internal/modules/run_main` because this is intended to run entry
  points with the ESM loader and not just any module.
- Documents how top-level await is handled.

PR-URL: nodejs#51999
Fixes: nodejs#42868
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
rdw-msft pushed a commit to rdw-msft/node that referenced this pull request Mar 26, 2024
PR-URL: nodejs#51999
Fixes: nodejs#42868
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
rdw-msft pushed a commit to rdw-msft/node that referenced this pull request Mar 26, 2024
When the entry point is a module and the graph it imports still
contains unsettled top-level await when the Node.js instance
finishes the event loop, search from the entry point module
for unsettled top-level await and print their location.

To avoid unnecessary overhead, we register a promise that only
gets settled when the entry point graph evaluation returns
from await, and only search the module graph if it's still
unsettled by the time the instance is exiting.

This patch only handles this for entry point modules. Other kinds of
modules are more complicated so will be left for the future.

Drive-by: update the terminology "unfinished promise" to the
more correct one "unsettled promise" in the codebase.

PR-URL: nodejs#51999
Fixes: nodejs#42868
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
baking-for-lts PRs that need to wait before landing in a LTS release. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Stalled top-level promise detection for TLA
8 participants