-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Investigate performance issues for experimental plugin #3598
Comments
nit:
Is this |
Current possible guess is the way we init wasm module - we instantiate it per each transform, which increases linear per input number grows. May try some attempt to reuse modules. |
Also adding the last comment I posted in the discussion thread (#3540): Simplifying it even further to exhibits the same problems: use swc_plugin::{ast::*, plugin_transform};
#[plugin_transform]
pub fn process_transform(program: Program, _config: String, _context: String) -> Program {
program
} It must be that the WASM runtime overhead that hits 4500 times as it needs to instantiate the plugin for each file? |
This is part I'm bit confused, are you saying you call swc inside of esbuild and with plugins? |
Yes, I do have an |
This is a simplified version of the As Instead of using const { parseFile, transformFile } = require('@swc/core');
const { Visitor } = require('@swc/core/Visitor.js');
class I18nVisitor extends Visitor {
constructor() {
super();
this.strings = [];
}
visitCallExpression(node) {
if (node.callee.type !== 'MemberExpression') {
return super.visitCallExpression(node);
}
const isI18nObject =
node.callee.object.type === 'Identifier' &&
node.callee.object.value === 'i18n';
const isGetProperty =
node.callee.property.type === 'Identifier' &&
node.callee.property.value === 'get';
if (!isI18nObject || !isGetProperty) {
return super.visitCallExpression(node);
}
if (node.arguments.length === 0) {
throw new Error(
`Invalid number of arguments to 'i18n.get' call! One argument is required.`,
);
}
const arg = node.arguments[0].expression;
if (arg.type !== 'StringLiteral') {
throw new Error(
`Invalid argument for 'i18n.get' call! Only string literals are allowed.`,
);
}
this.strings.push(arg.value);
return super.visitCallExpression(node);
}
visitTsType(node) {
return node;
}
}
const i18nPlugin = () => ({
name: 'i18n-plugin',
setup(build) {
const strings = [];
// /^(?!.*\.d\.tsx?$).*\.[tj]sx?$/
build.onLoad(
{ filter: /\.(js|jsx|ts|tsx)$/ },
async ({ path: filePath }) => {
const visitor = new I18nVisitor();
// Using 'parseFile' API (fastest)
// const module = await parseFile(filePath, {
// syntax: 'typescript',
// tsx: true,
// });
// visitor.visitModule(module);
// Using 'transformFile' API
// await transformFile(filePath, {
// syntax: 'typescript',
// tsx: true,
// plugin: module => visitor.visitProgram(module),
// });
// Using 'transformFile' API with Rust (WASM) plugin (slowest)
await transformFile(filePath, {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
experimental: {
plugins: [['./swc_i18n.wasm', {}]],
},
},
});
strings.push(...visitor.strings);
return undefined;
},
);
build.onEnd(async () => {
// eslint-disable-next-line no-console
console.log(
`[i18n-plugin] Found ${strings.length} strings!`,
);
});
},
});
module.exports = i18nPlugin; |
What happens if you isolate to invoke swc only? I doubt it makes huge difference but for better isolation between multiple processes. |
I've made this simple script to take esbuild out of the equation and just run SWC on the same file over and over again, once with the WASM plugin (no-op) and once without: const { transformFile } = require('@swc/core');
const filePath = './path/to/file.tsx';
async function main() {
for (let i = 0; i < 1000; ++i) {
await transformFile(filePath, {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
// Commented this out for the second run
experimental: {
plugins: [['./swc_i18n.wasm', {}]],
},
},
});
}
}
main(); The first run (with WASM plugin enabled):
The second run (with WASM plugin commented out):
|
Does our in-memory bytecode cache work with js calls? |
Theoretically, bytecode cache itself is being shared across transform* calls. If we see overhead, probably it's coming from actual module instantiation. |
OK, this is baseline script I'll try to dig further. const { transform } = require('.');
const { PerformanceObserver, performance } = require('perf_hooks');
const EXECUTE_COUNT = 5000;
const obs = new PerformanceObserver((items) => {
console.log(items.getEntries()[0]);
performance.clearMarks();
});
const arr = [...Array(EXECUTE_COUNT).keys()];
obs.observe({ entryTypes: ['measure'], buffered: true });
performance.mark("scheduleTask-start");
let tasks = arr.map((value) => transform(`console.log(${value})`, {
jsc: {
experimental: {
plugins: [[`${pluginPath}`, {}]]
}
}
}));
performance.mark("scheduleTask-complete");
performance.measure("scheduleTask", "scheduleTask-start", "scheduleTask-complete");
performance.mark("executeTask-start");
Promise.all(tasks).then(() => {
performance.mark("executeTask-complete");
performance.measure("executeTask", "executeTask-start", "executeTask-complete");
});
|
Ok, could confirm module init caching is the first notable offender. Will work on revising cache logic. |
👋 hey @kwonoj, am I correct in thinking that this landed in I'm using
|
@joshcartme It sounds like bit different to the perf issue itself. Those specific path is currently under basic test coverage (https://github.com/swc-project/swc/blob/main/crates/swc_plugin_runner/tests/integration.rs#L91) which we haven't observed since. I know you just mentioned it's from barebone plugin, but can you try to create reproducible repo to share? |
@kwonoj yeah let me see if I can come up with a repro. Our build has a huge number of files and I'm wondering if it has to do with that. |
@joshcartme I recall we used to use thread-loader for the webpack. Is it gone with swc-loader? and also are there some parallel thread / processes pushes memory pressure? |
@kwonoj I did get rid of
And the error I'm getting is:
|
I see. Please share repo once possible. Given stack it looks like it's more close to incorrectly deallocating memory instead of oom, but might need to peek. |
@kwonoj ok this is a very contrived example. It's not even really valid, but it fails in the same way:
.swcrc (the plugin is unmodified output from creating a new plugin):
Then:
fails with the same error. If you uncomment the line that defines factory, it works. Or if you don't do that and comment out any line inside the arrow function it works. |
I just discovered that if |
👋 ok @kwonoj here's a repro https://github.com/joshcartme/swc-oob-repro. I put a README with some instructions but happy to explain more as is helpful |
@joshcartme it.. works on my machine? :lolsob: |
oh boy! Did you build the plugin with
or
? |
I did try both just to be sure. |
Is there a way I could share the built wasm file I have with you to see if that makes a difference? edit: ooh and I wonder, I'm on os x, maybe it's an operating system specific thing |
I think this #3732 might fix issue. Let's try once this goes in and new version published. |
Sounds good, thanks! fwiw, I've tried this on my work computer macOS 10.15.7 and my personal computer macOS 11.6 and they both fail in the same way |
As #3732 is a change of plugin macro, it can be tested without a new version of |
@kdy1 can I test it now, and if so how? |
Yes, and by upgrading the macro crate |
Sorry to be dense, I'm pretty new to rust and SWC, is that just:
? |
Oh so I think the answer to my question was that no, that's not the way. I added
to my |
@joshcartme try |
Ok, with the update I can no longer repro on https://github.com/joshcartme/swc-oob-repro 🎉 I'm going to test things out in my bigger codebase and will report back. Thanks @kwonoj and @kdy1! |
I'm not getting |
With recent updates, I think this is reasonably resolved. We don't want to track all of performance issues in a single issue, so let me close this. If this is still a problem please share repro. Otherwise, please create a new issue. |
This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you. |
I wrote a fairly simple plugin to extract the first string literal argument from calls to
i18n.get
, in order to collect translation strings that are used.My goal was to transform an input file into a JSON array of strings. Please disregard that it currently will emit a semicolon at the end of that array :-)
Now when trying this WASM plugin, it is a lot slower than the equivalent JavaScript version while processing about 4500 files individually. The JavaScript version takes about 5 seconds (using
parseFile
+visitor.visitModule
), while the Rust WASM plugin version takes about a minute (using a lot more CPU as well!) and sometimes actually crashes with (likely caused by my lacking knowledge of writing code in Rust):It is invoked via
transformFile
inside anesbuild
build script as follows:Using
@swc/core
version1.2.140
(JS) &swc_plugin
version0.27.0
(Rust)I hoped I could speed things up even further using a native plugin, but got the opposite. What am I doing wrong? :)
Originally posted by @fxb in #3540 (comment)
The text was updated successfully, but these errors were encountered: