-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
--enable-source-maps is unnecessarily slow with large source files #41541
Comments
cc @bcoe (Thanks for the report!) |
As a side note - the best-practice I am aware of it not to run in production with source-map-support (or --enable-source-maps) and instead upload the sourcemaps to your error monitoring tooling. Example with sentry and elastic (since there is an OS offering) - https://www.elastic.co/guide/en/apm/agent/rum-js/current/sourcemap.html https://docs.sentry.io/platforms/javascript/sourcemaps/ |
I'd be curious to know where the hotspot is in execution, to see if it can be improved... But performance of the actual generation of stack traces hasn't so far been a concern, because this generally happens in the exceptional flow of an application and not on a hot path like in your example. If I'm understanding your example code: http.createServer((request, response) => {
response.writeHead(200);
console.log(new Error().stack);
response.end();
}).listen(3000); Every single request to the server your benchmarking is generating a remapped stack trace? Do you see a similar performance hit with large source maps in an example where no stack trace is created?
Source maps are no longer experimental, it's just the case that this paticular performance concern hasn't been analyzed or optimized (edit: if this is an actual use case we should fix it, I would much rather this than push users away from the feature in our docs.) Might be a good potential first contribution for somone to make (if you're perhaps even interested @cstickel). I would bet that a a CPU profile of your example might point to an obvious step in the algorithm to improve. |
(the fact that requests get slower and slower seems to point to a memory leak, which we definitely should fix). |
I created the repo node-issue-41541 to test the hypothesis of a memory leak. Across ab -n 40000 -c 12 http://127.0.0.1:3000/
Requests per second: 106.13 [#/sec] (mean)
Time per request: 113.070 [ms] (mean)
Time per request: 9.423 [ms] (mean, across all concurrent requests)
Transfer rate: 7.77 [Kbytes/sec] received Memory usage: If I had to guess the main cause of slowdown, it would be that the server is CPU bound (and the main culprit might be the I'm not sure if this is a feature that source-map-support has? So it perhaps doesn't have the need to read anything off disk like this? I feel like peformance-wise, for what's hopefully only coming up in exceptional cases, this throughput is probably pretty reasonable. But, would happily accept patches to improve CPU performance. |
@cstickel if you are able to build Node from source, this branch tests the hypothesis that the call to I noticed ~100% better benchmarks with that branch:
A bigger discussion to have, is it worth adding the complexity to optimize this path? If your application is throwing 1000s of stack traces, the performance degredation might not be your biggest problem. |
@bcoe this optimization seems reasonable though it will need to deal with the case the files changed (maybe by |
The thing about these files, is that they're likely to be files that were never in the import graph ( @cstickel you're example is actually a worst case scenario, because the source-map maps back to a file that's
I think this could work, I'd be curious to see if we get signficantly better performance with a sync stat call, than reading the file (I'm betting yes, but might be worth sending a branch to @cstickel to test 😃). |
But they can still change can't they? Wouldn't they be changing in a workflow where I develop |
@bcoe to be clear while I am thankful for the bug report by OP - I think the current behaviour is totally fine and you went above and beyond investigating. In the past error-cases were considered exceptional and the performance of doing I think it might make sense to improve the docs regarding expectations users should have with (Caching is probably fine but probably not a priority?) |
ah, sorry for the late response, was quite busy at the weekend. @benjamingr @bcoe source-map-support
native with caching
in general I'm for sure always interested in contributing to open source projects. however i'm not sure if I can handle it in a sane amount of time, because I have no clue about the project structure. but will have a look the next days, if I can wrap my head around. just regarding the discussion of the change of the source-maps. in which cases is that important? at least when performance matters (like in production) it's not likely to change without a full restart. so it might also be possible to provide an additional parameter to control how aggressive the cache is. but that might make it a way to complicated :-D does source-map-support handle those cases properly? @benjamingr and @bcoe regarding if it's worth to fix/improve Of course that's only the case if you don't say using In any case, it just seems to be a regression from source-map-support or is there any reason why the native solution performs that much worse? Are there additional features or bugs fixed that require a lot more processing? However, I also don't see it as a top priority. For me personally I'll try to get the source maps to the monitoring as suggested by @benjamingr. It really just sounds like a good practice, just wasn't aware of it. In cases where this isn't possible I'm going to still use source-map-support as long as it's maintained, because it's working perfectly fine. It's an additional dependency, but... that's just how it is. Also not bundling to a single file would for me be an option, if that improves the performance, but actually I do like the small build artifacts that can be achieved with the bundling (no overhead from node_modules). So would be happy if the issue is down prioritized, but not closed, because I personally do see a good reason why the performance should be improved in the long term :-D |
@cstickel If you could figure out a minimal reproduction that demonstrates behavior closer to this, it would help me decide if it's worth implementing caching (given the complexity it introduces). I'm not 100% sure why the performance is significantly worse than If we can cause the slowdown you're describing, perhaps we can find some other optimizations while we're at it. |
thanks that you put so much effort into the topic! |
@bcoe sorry didn't find time yesterday, but there it is: https://github.com/cstickel/node-js-source-maps-performance-test for me the effect is heavily visible in that example for 20 req/sec. however in a real application it was even a bit worse. think main cause are more dependencies => bigger bundle size than in the stripped down example. and also disk i/o might be worse in kubernetes. |
@cstickel thank you for the reproduction. I'm unlikely to have time to dig again until the weekend. When I have a chance to, I will try the recommendation of using |
I use nest js and found this strange behavior, when webpackHotReload my "empty" application restarts for 4 seconds with the time node --require=source-map-support/register dist/server/server.dev.js
time node --enable-source-maps dist/server/server.dev.js
This makes me sad because I can't manage file paths in the source-map-support plugin, but it works very fast. And the file paths in native nodejs are fine, but waiting 4 seconds after each edit to restart my server is a pain. I understand that this is not really about the performance of the application, however, suddenly it will be important here |
@WeslyG do you feel comfortable building Node.js from source? perhaps you could test against this branch: If this is biting people in the real world, let's fix it. |
In terms of real world, we've just upgraded from Node 12 to 16 and have seen our start times leap up from approx 15s to at worst over 90s (seems to always 60s+). Checking with I'm not sure if I can add much more to the conversation than that however. |
@ChrisWestcottUK are you running with |
Yes, it drops back down to about 10-15s without We've jumped up a few versions in one go so wonder if it is a regression in this area. |
I've just tested switching back to Node 14 LTS with the same problem. And then back to 12 again which has no such delays. |
I'm also running into this. Switching my Webpack devtool to "nosources-cheap-source-map" helped a little bit since we just need line numbers and not source content, but the hit is still pretty big. The source map isn't unreasonably large, about 300kb. |
@ChrisWestcottUK @laverdet if either of you feel comfortable building against a branch, try: The major difference between 12 and 14 is that the original source file is loaded from disk, so that stack traces can be shown in context. What's odd to me, is that it should be the original source being read from disk, not the bundled source so I'm a little surprised that there's such a performance hit -- this is why I'd like confirmation that the fix actually helps, in a real world environment, before adding the caching behavior. Note: if you can't build from source, alternatively you can point me against a test case to run against, and I can profile. |
Thanks. I didn't notice any difference in runtime when running with 21489fd. Our applications starts up in about 5 second with no flags, or 30 seconds with --enable-source-maps. |
@laverdet or @ChrisWestcottUK do either of you have a reproduction I could work from for profiling? |
I took some time this morning to look into the issue further and I think I know what's going on here. You can fix the issue by removing these debug invocations: diff --git a/lib/internal/source_map/prepare_stack_trace.js b/lib/internal/source_map/prepare_stack_trace.js
index 9502cfef6f..8f1438d243 100644
--- a/lib/internal/source_map/prepare_stack_trace.js
+++ b/lib/internal/source_map/prepare_stack_trace.js
@@ -106,7 +106,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
}
}
} catch (err) {
- debug(err.stack);
}
return `${str}${t}`;
}), '');
diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js
index c0de6aeb51..a2bb0ee9ef 100644
--- a/lib/internal/source_map/source_map_cache.js
+++ b/lib/internal/source_map/source_map_cache.js
@@ -79,7 +79,6 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
} catch (err) {
// This is most likely an [eval]-wrapper, which is currently not
// supported.
- debug(err.stack);
return;
}
const match = StringPrototypeMatch(
@@ -119,7 +118,6 @@ function dataFromUrl(sourceURL, sourceMappingURL) {
return null;
}
} catch (err) {
- debug(err.stack);
// If no scheme is present, we assume we are dealing with a file path.
const mapURL = new URL(sourceMappingURL, sourceURL).href;
return sourceMapFromFile(mapURL);
@@ -144,7 +142,6 @@ function sourceMapFromFile(mapURL) {
const data = JSONParse(content);
return sourcesToAbsolute(mapURL, data);
} catch (err) {
- debug(err.stack);
return null;
}
}
@@ -163,7 +160,6 @@ function sourceMapFromDataUrl(sourceURL, url) {
const parsedData = JSONParse(decodedData);
return sourcesToAbsolute(sourceURL, parsedData);
} catch (err) {
- debug(err.stack);
return null;
}
} else { The call in If these logs are important, consider some check before accessing the Consider: const d = performance.now();
try {
throw new Error("wow");
} catch (err) {
}
console.log(performance.now() - d);
const d2 = performance.now();
try {
throw new Error("wow");
} catch (err) {
err.stack;
}
console.log(performance.now() - d2); Logs: This is actually a problem elsewhere in the nodejs codebase: node/lib/internal/modules/esm/resolve.js Line 1183 in 6847fec
node/lib/internal/modules/esm/module_job.js Line 138 in 6847fec
The best practice in these cases, as far as I know, is to install a lazy Edit: |
@laverdet if we switched to Edit: also, should we open separate issues for const lines = StringPrototypeSplit(error.stack, '\n');? |
Refs: nodejs#41541 PR-URL: nodejs#42096 Refs: nodejs#41541 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Mestery <mestery@protonmail.com>
Refs: nodejs#41541 PR-URL: nodejs#42096 Refs: nodejs#41541 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Mestery <mestery@protonmail.com>
Refs: conversation in #43428 @legendecas I believe we solved one of the major sources of slowdown, which was that the However, in some of my profiling, I was noticing that A user in #43186 suggests dropping this prepended context, as it would make error messages more compliant with an emergent specification (proposal-error-stacks) -- I feel it might be worth dropping the logic for prepending context:
However, should this be considered a breaking change, as some people might have written code that assumes the context is prepended? |
It is a breaking change. However, as the source context is not prepended on the value of Ideally, the behavior should be only printing the source context when the exception is caught by the runtime and there is no proper handler to process the exception, just like when the source map is not enabled. I'll work on this to align the behavior on whether or not the source map is enabled, to improve the performance when people just like to get the value of the |
The source context is not prepended to the value of the `stack` property when the source map is not enabled. Rather than prepending the error source context to the value of the `stack` property unconditionally, this patch aligns the behavior and only prints the source context when the error is not handled by userland (e.g. fatal errors). Also, this patch fixes that when source-map support is enabled, the error source context is not pointing to where the error was thrown. PR-URL: #43875 Fixes: #43186 Fixes: #41541 Reviewed-By: Ben Coe <bencoe@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
The source context is not prepended to the value of the `stack` property when the source map is not enabled. Rather than prepending the error source context to the value of the `stack` property unconditionally, this patch aligns the behavior and only prints the source context when the error is not handled by userland (e.g. fatal errors). Also, this patch fixes that when source-map support is enabled, the error source context is not pointing to where the error was thrown. PR-URL: #43875 Fixes: #43186 Fixes: #41541 Reviewed-By: Ben Coe <bencoe@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
The source context is not prepended to the value of the `stack` property when the source map is not enabled. Rather than prepending the error source context to the value of the `stack` property unconditionally, this patch aligns the behavior and only prints the source context when the error is not handled by userland (e.g. fatal errors). Also, this patch fixes that when source-map support is enabled, the error source context is not pointing to where the error was thrown. PR-URL: nodejs#43875 Fixes: nodejs#43186 Fixes: nodejs#41541 Reviewed-By: Ben Coe <bencoe@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Found this issue and some tweets by @vvo after having this exact same problem on AWS Lambda. If you're using esbuild and you only care about stack traces of your own code, you can bypass the generation of sourcemaps for everything inside node_modules with a simple trick like this one: evanw/esbuild#1685 (comment). Mine went from a huge 24M to a mere 240K. If you're not using sourcemaps to debug the bundled code, another good alternative is to turn off the |
Version
v16.13.2
Platform
5.13.0-22-generic #22-Ubuntu x86_64 x86_64 x86_64 GNU/Linux
Subsystem
No response
What steps will reproduce the bug?
I know
--enable-source-maps
is experimental and it's maybe an edge case, because the js files are only large enough the show the effect if they are bundled, which might in most cases not be the case. However, hopefully the bug report still helps to improve the feature.tsc --sourceMap index.ts
--enable-source-maps
, likenode --enable-source-maps index.js
=> The first requests will be quite fast, but then they get slower and slower.
node index.js
node -r source-map-support/register index.js
=> considering source-maps obviously has a performance impact, but it's neglectable with the source-map-support library, even in production for most cases (even if there are a lot of accesses to error stacks)
How often does it reproduce? Is there a required condition?
Always if the javascript file is large enough and error stacks are accessed quick enough.
What is the expected behavior?
Similar performance to source-map-support.
What do you see instead?
A heavy performance impact for using native source map support.
Additional information
--enable-source-maps
is experimental and not meant to be used in production environments, but still I think this needs to be fixed if it ever should be non experimental.Some data from artillery, just for everyone who is not reproducing it on it's own (of course this stats heavily depend on the machine they are executed on, but the tendency should be the same everywhere):
With
node --enable-source-maps index.js
(native source maps)=> as it already stacks up load for more than 60 sec or more requests per secound would lead to timeouts
With
node index.js
(no source maps)10 times more requests
With
node -r source-map-support/register index.js
(source-map-support)10 times more requests
The text was updated successfully, but these errors were encountered: