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

Fix performance of constructor parsing rules #6869

Open
32 of 79 tasks
romainmenke opened this issue May 28, 2023 · 13 comments
Open
32 of 79 tasks

Fix performance of constructor parsing rules #6869

romainmenke opened this issue May 28, 2023 · 13 comments
Labels
status: wip is being worked on by someone type: bug a problem with a feature or rule

Comments

@romainmenke
Copy link
Member

romainmenke commented May 28, 2023

Recommended config:

  • annotation-no-unknown
  • color-no-invalid-hex
  • custom-property-no-missing-var-function
  • declaration-block-no-duplicate-properties
  • font-family-no-duplicate-names
  • font-family-no-missing-generic-family-keyword
  • function-calc-no-unspaced-operator
  • function-linear-gradient-no-nonstandard-direction
  • function-no-unknown
  • media-feature-name-no-unknown
  • named-grid-areas-no-invalid
  • no-descending-specificity (1687)
  • no-duplicate-at-import-rules
  • no-duplicate-selectors (363)
  • selector-anb-no-unmatchable
  • selector-pseudo-class-no-unknown (153)
  • selector-pseudo-element-no-unknown
  • selector-type-no-unknown
  • string-no-newline
  • unit-no-unknown

Standard config:

  • alpha-value-notation
  • color-function-notation
  • color-hex-length
  • custom-property-pattern
  • font-family-name-quotes
  • function-name-case
  • function-url-quotes
  • hue-degree-notation
  • import-notation
  • length-zero-no-unit
  • media-feature-range-notation
  • selector-attribute-quotes
  • selector-class-pattern (342)
  • selector-id-pattern
  • selector-no-vendor-prefix
  • selector-not-notation
  • selector-pseudo-element-colon-notation
  • selector-type-case
  • shorthand-property-no-redundant-values
  • value-keyword-case
  • value-no-vendor-prefix

Selector rules:

  • selector-attribute-name-disallowed-list
  • selector-attribute-operator-allowed-list
  • selector-attribute-operator-disallowed-list
  • selector-combinator-allowed-list
  • selector-combinator-disallowed-list
  • selector-max-attribute
  • selector-max-class
  • selector-max-combinators
  • selector-max-compound-selectors
  • selector-max-id
  • selector-max-pseudo-class
  • selector-max-specificity
  • selector-max-type
  • selector-max-universal
  • selector-no-qualifying-type
  • selector-pseudo-class-allowed-list
  • selector-pseudo-class-disallowed-list
  • selector-pseudo-element-allowed-list
  • selector-pseudo-element-disallowed-list

Other rules:

  • color-hex-alpha
  • color-named
  • color-no-hex
  • declaration-block-no-redundant-longhand-properties
  • declaration-property-max-values
  • declaration-property-unit-allowed-list
  • declaration-property-unit-disallowed-list
  • declaration-property-value-no-unknown
  • font-weight-notation
  • function-allowed-list
  • function-disallowed-list
  • max-nesting-depth
  • media-feature-name-unit-allowed-list
  • media-feature-name-value-allowed-list
  • no-unknown-custom-properties
  • number-max-precision
  • time-min-milliseconds
  • unit-allowed-list
  • unit-disallowed-list

List created with :

find ./lib/rules -type f -print0 | xargs -0 grep -l "parse"

Some rules may not have any issues. Maybe they just contain the word "parse" in a comment.


In stylelint/css-parser#2 performance was discussed in the context of CSS parsers.

This made me curious about the current state of Stylelint.
Is it fast, slow, ... ?

This issue doesn't say anything about stylelint/css-parser#2.
The other issue was merely inspiration for this one.


Especially for Stylelint performance is an interesting subject because by it's very nature Stylelint runs should gravitate towards becoming a noop.

In a codebase with many linter errors it's logical to do a lot of work to compute what the issues are and where they are.

However in a codebase that is already "healthy", Stylelint should be able to skip a lot of work.
Optimizing for codebases that are already free of issues means that users will have a fast experience most of the time.

Even in a codebase that is full of errors, there is a lot parsing and processing done that can be eliminated. Most parts of a CSS codebase do not apply to most Stylelint rules.

A low effort and low complexity way to make Stylelint faster is to add one or two types of fast aborts :

  1. the rule doesn't apply to the CSS source
  2. the rule does apply but it will definitely pass with a given config

Example of 1 :
color-function-notation where the CSS source doesn't contain rgb|rgba|hsl|hsla

Example of 2 :
color-function-notation with option modern where the CSS source doesn't contain any comma's.

These checks will typically be regexp's that are to broad to know if the issue is real and where it is, but are specific enough to know that there can not be an issue.

i.e. they can have false negatives, but not false positives.


I've already explored some rules for low hanging fruit and I think reviewing all (non deprecated) rules and making improvements to them will be good for users of Stylelint.

@Mouvedia
Copy link
Contributor

If you gotta have a shortlist of rules that you wanna review pick in priority the ones included in stylelint-config-recommended.

@ybiquitous ybiquitous added status: wip is being worked on by someone type: refactor an improvement to the code structure labels May 29, 2023
@ybiquitous
Copy link
Member

@romainmenke Thank you so much for your effort! 👏🏼

@romainmenke
Copy link
Member Author

romainmenke commented May 29, 2023

Edit: Todo list moved to the top post.

edit: added config-recommended sub list

@romainmenke
Copy link
Member Author

romainmenke commented Jun 16, 2023

🤔 selector parsing seems to be particularly slow.

With a modified benchmark source that includes :

  • bootstrap
  • open props
  • tailwind

these three are only chosen because they are readily available and large

selector-pseudo-element-no-unknown
Warnings: 0
Mean: 193.79829168749998 ms
Deviation: 14.546288406300306 ms

baseline
Warnings: 1
Mean: 32.53261711798942 ms
Deviation: 27.53034442617133 ms

@ybiquitous
Copy link
Member

Benchmarking for CSS code including more modern syntax sounds like a good idea. The current CSS (Bootstrap 4.4) might be a bit outdated. 🤔

const CSS_URL = 'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.css';

@romainmenke
Copy link
Member Author

selector parsing seems to be particularly slow

Seems to be caused by the rule.selectors getter property of PostCSS.
This getter splits by comma but does so with a tokenizer-like script.

There are two things affecting performance here :

scenario A :

Seemingly simply checks are done against rule.selector multiple times.
Each time the getter is used the splitting happens again and again.

scenari B :

The selectors need to be parsed but parsing isn't done with the selector list as a whole.

Just passing everything to the selector parser would be much faster because of multiple reasons :

Multiple short/small loops are a lot slower than one longer loop:

  • cpu cache hit rate
  • array access optimizations
  • ...

By first splitting with rule.selectors and then parsing the work is done twice and the work is done in smaller chunks.


Refactoring rules to always use rule.selector and parsing that with postcss-selector-parser would be better.


Other note :

Including TailwindCSS in my temporary benchmark skewed the results too much.

TailwindCSS isn't representative of typical CSS because it's ratio of selectors vs. properties is off and it's selectors have too much escaping.

Optimizing specifically for TailwindCSS isn't that interesting in my opinion.
Better to optimize for CSS as a whole. TailwindCSS users will still see the benefits of that.

@ybiquitous
Copy link
Member

Consideration about rule.selector performance makes sense. 👍🏼

@jeddy3 jeddy3 changed the title Improve performance of various rules Fix performance of constructor parsing rules Jun 24, 2023
@jeddy3 jeddy3 added type: bug a problem with a feature or rule and removed type: refactor an improvement to the code structure labels Jun 24, 2023
@jeddy3
Copy link
Member

jeddy3 commented Jun 24, 2023

This is a great initiative. It reminds me of this series of articles that look into improving JavaScript tool performance.

I think it makes sense to limit this issue to:

  • adding either of the two fast aborts to avoid unnecessarily using the construct parsers
  • aborting as early as possible
  • always using rule.selector and parsing that with postcss-selector-parser

I've:

  • moved the list of rules to the top post (to form a tasklist)
  • identified the standard config rules
  • identified selector rules
  • removed the deprecated rules

@Mouvedia
Copy link
Contributor

Mouvedia commented Jun 24, 2023

Since performance is a never ending endeavour I think we should state the minimal outcome for this issue to be closed.
My proposal: once @romainmenke or other contributors have reviewed all rules that are either recommended or standard for potential performance improvements and submitted PRs or specific issues in accordance, it should be closed.
i.e. not all rules will require a PR hence just reviewing can result in a checkmark

@jeddy3
Copy link
Member

jeddy3 commented Jun 25, 2023

not all rules will require a PR hence just reviewing can result in a checkmark

That's correct. It was implied, but I should've made it explicit for clarity.

I think we should state the minimal outcome for this issue to be closed

We can close once all the tasks in the list are done, i.e. each rule is checked and any necessary changes made.

The list is in priority order, but people are free to choose which rule(s) they want to look at.


As an aside, I've unassigned @romainmenke from the issue. We don't typically assign other people to issues because we're an open-source project where everyone can pick up or ignore any issue they want. However, people are welcome to assign themselves if they find it useful for their workflow. I'll rustle up a quick PR to add this to the managing issues part of our maintainer guide, where there's guidance on other things like how we use labels.

@romainmenke
Copy link
Member Author

This is starting to have noticeable impact :)

A commit from before this effort was started : 369557b79805ca3daee5d595662ba603f27ba9c2

Warnings: 3055
Mean: 231.0711666666667 ms
Deviation: 16.11884950316728 ms

On the branch for no-descending-specificity :

Warnings: 2700
Mean: 183.03177293103448 ms
Deviation: 12.544221828223176 ms

Benchmark script :

/* eslint-disable no-console */
import Benchmark from 'benchmark';
import picocolors from 'picocolors';
import postcss from 'postcss';

import stylelint from '../lib/index.js';

const { bold, yellow } = picocolors;

const CSS_URL = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.css';

const processor = postcss().use(stylelint({ config: { "extends": "../stylelint-config-standard" } }));

fetch(CSS_URL)
	.then((response) => response.text())
	.then(async (response) => {
		let css = response;

		console.profile(`profile__config-standard`);
		await processor.process(css, { from: undefined }).then((result) => {
			result.messages
				.filter((m) => m.stylelintType === 'invalidOption')
				.forEach((m) => {
					console.log(bold(yellow(`>> ${m.text}`)));
				});
			console.log(`${bold('Warnings')}: ${result.warnings().length}`);
		}).catch((err) => {
			console.error(err.stack);
		});
		console.profileEnd(`profile__config-standard`);

		let lazyResult;

		const bench = new Benchmark('rule test', {
			defer: true,
			setup: () => {
				lazyResult = processor.process(css, { from: undefined });
			},
			onCycle: () => {
				lazyResult = processor.process(css, { from: undefined });
			},
			fn: (deferred) => {
				lazyResult.finally(() => {
					deferred.resolve();
				});
			},
		});

		bench.on('complete', () => {
			console.log(`${bold('Mean')}: ${bench.stats.mean * 1000} ms`);
			console.log(`${bold('Deviation')}: ${bench.stats.deviation * 1000} ms`);
		});

		bench.run();
	})
	.catch((error) => console.error('error:', error));
/* eslint-enable no-console */

@romainmenke
Copy link
Member Author

romainmenke commented Jul 7, 2023

Context :

Warnings: 2700
Mean: 156.39324757575756 ms
Deviation: 12.296275351168699 ms

@jeddy3
Copy link
Member

jeddy3 commented Jul 7, 2023

Nice to see your upstream changes in PostCSS making a dent in those timings!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: wip is being worked on by someone type: bug a problem with a feature or rule
Development

No branches or pull requests

4 participants