coderaiser / putout

🐊 Pluggable and configurable JavaScript Linter, code transformer and formatter, drop-in ESLint superpower replacement 💪 with built-in support for js, jsx, typescript, flow, markdown, yaml and json. Write declarative codemods in a simplest possible way 😏
https://putout.cloudcmd.io/
MIT License
698 stars 40 forks source link

[Plugin Proposal]: `plugin-speedify` to apply small inline code optimizations. #174

Closed ElPrudi closed 1 year ago

ElPrudi commented 1 year ago

@putout/plugin-speedify idea

Why ?

The idea of this plugin is that certain common micro-optimizations, which are easy to overlook, are automatically corrected and / or adjusted. Alone, these optimizations are not worth mentioning, but in larger code projects, where they appear hundreds or thousands of times, can make up for a few seconds of computing time.

In the following, these small optimizations are listed categorically and can be expanded in the replies. This plugin, if well developed, could bring a big advantage over other Eslint plugins and thus a better overall code quality level.

Some of these ideas are already housed in other plugins, however it would be recommended to move those with the goal of increasing performance to this plugin.

Rules

Number

for (let i = 0; i < 1000000; i++) +i // 2.614ms

- Replace `Infinity` with `Number.MAX_VALUE` and `-Infinity` with `Number.MIN_VALUE`:
```ts
for (let i = 0; i < 1000000; i++) if (i > Infinity) i++ // 3.697ms
for (let i = 0; i < 1000000; i++) if (i > Number.MAX_VALUE) i++ // 2.118ms

String

Boolean and Checks

for (let i = 0; i < 1000000; i++) i instanceof String // 8.233ms for (let i = 0; i < 1000000; i++) typeof i === 'string' // 2.401ms

for (let i = 0; i < 1000000; i++) i instanceof Boolean // 8.891ms for (let i = 0; i < 1000000; i++) typeof i === 'boolean' // 2.449ms

for (let i = 0; i < 1000000; i++) i instanceof BigInt // 8.192ms for (let i = 0; i < 1000000; i++) typeof i === 'bigint' // 2.46ms

for (let i = 0; i < 1000000; i++) i instanceof Symbol // 10.366ms for (let i = 0; i < 1000000; i++) typeof i === 'symbol' // 2.481ms


### Date

- Replace all `+date` and `date.valueOf()` with `date.getTime()`:
```ts
for (let i = 0; i < 1000000; i++) +(new Date()) // 190.873ms
for (let i = 0; i < 1000000; i++) new Date().valueOf() // 134.638ms
for (let i = 0; i < 1000000; i++) new Date().getTime() // 131.248ms

Array

Objects

coderaiser commented 1 year ago

Looks good! The only thing I would prefer name similar to minify, speedify, for example.

Will you help me with transformations? You can put them to 🐊Putout Editor, as I see most of them pretty simple and can be made using 🦎PutoutScript only.

And some of them we already have:

This one is good for @putout/plugin-math:

This one is good for @putout/plugin-minify:

We can just add speed comparison to documentation.

ElPrudi commented 1 year ago

You can choose the name as you like. I have no problem with that.

I could try to help you with the transformations, but I can't promise much. Not everything has to be included though, they are just ideas.

coderaiser commented 1 year ago

Replace all instanceof of primitive types with typeof

Not sure if it's safe transformation:

image

ElPrudi commented 1 year ago

Extending primitive types and built-ins is widely considered an anti-pattern and is extremely slow and memory intensive and is extremely rare to find today. For example, it costs 1 KB of heap to add a function to Math while a standalone function costs 200 B. I once had all the values written down, but I can't access the list right now, but I'll put them in here later.

So it is a great idea to add a rule to not extend it.

coderaiser commented 1 year ago

So it is a great idea to add a rule to not extend it.

How the code can look like? Before and after transformation.

coderaiser commented 1 year ago

You know so much about optimizations, could you help me to optimize @putout/compare?, this is the function used most of the time in all plugins. I did my best, but I feel that I could miss something. Optimizing this function will speed up 🐊Putout drastically.

ElPrudi commented 1 year ago

So it is a great idea to add a rule to not extend it.

How the code can look like? Before and after transformation.

Extensions of built-ins look like this:

<Built-in>.prototype.<function> = {} // instance method
<Built-in>.prototype.<property>  = 5 // instance property
<Built-in>.<function> = {} // static method
<Built-in>.<property> = 5 // static property

If you extend it with a class, it looks like this:

class <Name> extends <Built-In>

It's not fixable, you should just forbid it like eslint-plugin-no-use-extend-native does.

While it's ok to extend Error, Array, Set, Map, WeakMap, WeakSet, the TypedArrays or other data structures, extending Math, Boolean, Number and String should be avoided.

ElPrudi commented 1 year ago

You know so much about optimizations, could you help me to optimize @putout/compare?, this is the function used most of the time in all plugins. I did my best, but I feel that I could miss something. Optimizing this function will speed up 🐊Putout drastically.

I can look into it. It's what I enjoy the most. But you should first consider using TypeScript and build your plugins with esbuild. This will significantly decrease the import time of the plugins and will speed up stuff significantly more than you think.

ElPrudi commented 1 year ago

This one is good for @putout/plugin-math:

  • Replace x * 0.333333 or x ** (1 / 3) with Math.cbrt(x)

One thing about that: I'd suggest that the 0.333333... should be checked for any length, as Math.cbrt(x) is much more precise.

coderaiser commented 1 year ago

It's not fixable, you should just forbid it like eslint-plugin-no-use-extend-native does.

All 🐊Putout rules are fixable, and if this features exists in eslint-plugin-no-use-extend-native it can be just used as it is.

But you should first consider using TypeScript and build your plugins with esbuild.

TypeScript is too slow, I'll wait while it will be rewritten to Rust. Also all this types doesn't help me at all. TypeScript gives to much freedom in naming methods and options: when you know there is autocomplete, you can name a thing with aLotOfWordsSinceTheyWillBeCompleted, and I'm building simple API, which can be memorized easily. Also I'm using VIM, so don't know how TS help me.

The idea of independent plugins, that can be loaded when needed it is one of design choices, anyways I have bundle support, with help of rollup, it is used in Mobile Putout Editor.

ElPrudi commented 1 year ago

TypeScript is for readability only. I don't know what form your objects, options, and arguments have. It also helps you narrow down types and find bugs that aren't so obvious. Even if it's a simple API, it's hard to fix something through a PR if people don't understand what a function does and what types are used, thus taking more time to understand what's what than to actually fix the code. You say it's easy to understand, but you don't even have documentation comments.

I didn't say bundle every plugin into one big bundle. I meant that you need to minify your plugins to save import time. The file you linked is 6.56 KB in size. I parsed it with esbuild, and now it is only 4.2 KB with the same functionality. That doesn't sound like much, but it saves a lot of import time doing it with each plugin separately.

The reason I said you need to parse it with esbuild is because it is the fastest minifier and bundler out there. It even has a rollup plugin if you want to bundle your plugins with rollup.

coderaiser commented 1 year ago

I know about esbuild, it has a lot of issues, and things that it still do not support.

About TS: I also don’t like it’s messages like: “ambient context”, error codes, and inability to fix things. Take a look at commits that points to issues you create. They all are similar, they all very simple and have tests. Types not needed at all for this cases.

About comments, I don’t write them, I write documentation. Everything in README files.

About imports, as I said would be great to optimize compare function. Look 🐊Putout right now has a lot of scenarios, that includes: CLI, editor, mobile editor, coverage , mocking, minifiier, Deno support etc. Some of them use bundling, some not. There is no need to switch language (except to Rust, maybe in future) since I have everything that I need to move very fast, with fast tests and fast way to make transformations.

ElPrudi commented 1 year ago

It's simple: if you don't explain your functions or types in your code, you can't expect anyone to help you fix something you can't. Imagine that babel exposes its utility functions, but has no documentation comments because it is "a simple API".

Minimizing your code is essential when you load so many plugins at once. Because otherwise you will also load your comments, lengthy variable and function names and empty lines. The JS interpreter has to load the entire file before it can decide what to import and what not. And from the example above, loading 4.2 KB instead of 6.56 KB is faster. I only named esbuild because it's the fastest. You can use terser, swr or any other minifier.

You keep complaining that TS is slow, lengthy and incomprehensible. I can tell you that very loose TypeScript exists. You don't need to write verbose and complex types. "Ambient context" is only a thing if you include declaration files. Don't. You don't need declaration files to use types. You can just import them like normal things. TypeScript even has type imports to remove those imports after compilation.

coderaiser commented 1 year ago

OK, if you help me with .dts file for plugins api and going to write plugins using TypeScript, I’ll merge it. We can start from the simplest type of plugin: Replacer, from 💚@putout/engine-runner written in 🦎PutoutScript:

export const report = () => 'always string';

export const replace = () => ({
    'const always = "string"': 'const also = "string"',
});

☝️You can read more about Plugins API and even bootstrap your own plugin with yo putout

image ☝️There is a yellow Replacer on schema

How types can look like? Would be great if they can check what is exported, since report is always Report type arrow function that returns a string and cannot have any other type.

About bundling:

I know that this speeds things up, but I need a way of simple debug for such big application, it makes life easier, anyways you can use 🐊Putout as library and import only plugins you need, and also use esbuild it will speed up lint for you drastically.

Also I need to feel the speed with real imports, so I can speed up the most important things, like compare (run, not load). This is HOT function, and speed things up win influence all consumers, not only CLI, so the win is really important 80% Pareto Principle.

☝️ Here is brunch with first class support of TypeScript, without building to separate directory, like in Deno feature/typescript-support-for-plugins. But I don’t like both things: slow speed and way 🐊Putout plugins looks like.

ElPrudi commented 1 year ago

I'll help you with the compare plugin as most of its functions are quite easy to understand. The thing is: You don't need a .d.ts file. There are multiple tools that can automatically generate the .d.ts files for the current project. You only need to copy the documentation comments of your functions afterwards. We only need to write normal TypeScript. The easiest way to do it is by using a rollup config, as it has plugins for type checking and automatic declaration file generation.

I can help you with the setup. We can try minifying and bundling either with esbuild or terser. Or you can make your own rollup plugin of your bundler and minifier. Here is how to create your own rollup plugin.

coderaiser commented 1 year ago

Thank you, but I don’t want build anything in this project, I know how it slow, and I don’t need this. As I said, I need a way to debug easily and fast tests. Right now 4000 tests passed in less then 30 seconds. This is the reason why I don’t want support recast anymore it has very slow tests, because of build step and TS which never helps to fix bugs. I like JavaScript and maximum what I add is .dts file and even that I don’t need.

The thing is for all issues you created TS would help to fix none of them.

ElPrudi commented 1 year ago

Other than that, I only found three main performance caveats:

  1. You create static objects for each function call, even though they are readonly.

    let x
    for (let i = 0; i < 1000000; i++) xyz = { name: ANY}.name // 4.002ms
    const any = { name: ANY}
    for (let i = 0; i < 1000000; i++) xyz = any.name // 1.303ms
  2. You don't hoist variables in your loops

    
    for (let i = 0; i < 1000000; i++) {   // 2.501ms
    const x = i
    const y = i + 1
    const z = i + 2
    }

let x1, y1, z1 for (let i = 0; i < 1000000; i++) { // 1.2ms x1 = i y1 = i + 1 y1 = i + 2 }


3. You rely to much on objects and arrays. You create them, just to destructure them again later. Destructuring is slow. Everytime you use `{...}` or `[...]` costs time.
```ts
const obj_ID = { name: 'ID', key: '123' }
for (let i = 0; i < 1000000; i++) { // 2.874ms
    const { name, key } = obj_ID
}

let name, key
for (let i = 0; i < 1000000; i++) { // 1.249ms
    name = obj_ID.name
    key = obj_ID.key
}

These are the only things I can find. More structural performance issues require a deeper knowledge of putout.

coderaiser commented 1 year ago

Could you please point me on places in code, that can be improved?

coderaiser commented 1 year ago

Closed due to a long time of inactivity 🏝 Feel free to reopen when you have new ideas.