Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

refactor(rome_formatter): Flat IR #3160

Merged
merged 5 commits into from
Sep 23, 2022
Merged

refactor(rome_formatter): Flat IR #3160

merged 5 commits into from
Sep 23, 2022

Conversation

MichaReiser
Copy link
Contributor

@MichaReiser MichaReiser commented Sep 5, 2022

Summary

This PR changes our hierarchical Formatter IR to a flat IR as described in #2571.

The formatter IR today is a deeply nested structure where every container (group, indent, ...) wrap some inner content. The downside of this is that the formatter must allocate a new Vec for every single container which is expensive.

This PR changes the formatter IR and replaces (almost all) containers with Signals. A signal signals adds some formatting to all content up to its corresponding end signal.

// Before:
Text,
Group([
  Text, 
  Line
])

// After
Text,
Signal(StartGroup),
Text, 
Line
Signal(EndGroup)

The upside of using a flat structure is that it is no longer necessary to allocate a new Vec for every container, which drastically reduces the number of allocations in the formatter.

The main downside is that it is now possible to construct invalid documents if the start and end signals don't match. This concern is mitigated by the fact that very few places in the formatter construct the IR elements manually using FormatElement. The vast majority uses the builders provided by rome_formatter that are well tested to only create valid documents.

Nevertheless, the fact that invalid documents are a possibility requires to change the Printer::print method to return a PrintResult so that it can return an Err if an invalid document is provided.

The changes are mostly local to the rome_formatter crate, proving that we have good abstractions in place.

Format Element

This PR removes all FormatElements with a Box<FormatElement> and replaces them with a new FormatElement::Signal element. A Signal always comes in a Start and End pair where the Start may store additional information, e.g. the group id.

The two containers that haven't been moved out are:

  • FormatElement::Interned: It's sole purpose is to store a Box<FormatElement> to get cheap writes (and clones).
  • FormatElement::BestFitting: It would be possible to model BestFitting with a Start/End signal but it significantly increased complexity when measuring if a variant fits because it becomes necessary to skip over later coming variants. That's why I left it for now. Using BestFitting comes at a high cost anyway because it requires interning the inner content, and nested best fittings have exponential complexity when printing.

Builders

It has been necessary to change the container (group, indent, ..) builders to now write start/end signals instead of creating a new VecBuffer, writing into that buffer, and then creating an element wrapping the content.

Printer

It has been necessary to replace the internal queue of the printer where it stored each element to print together with its PrintElementArgs with two data structures:

  • CallStack: Stack storing the PrintElementArgs. A new entry is added when printing a start signal and the entry gets popped with the end signal.
  • PrintQueue: A queue tracking which element must be printed next. The queue uses an internal stack of slices to process together with an index pointing to the next element. This avoids copying all elements into the internal queue which would be especially expensive during measuring if the content fits because it would be necessary to copy a whole group or entry into the queue.

Performance

Linux

group                                    comments                                flat
-----                                    --------                                ----
formatter/checker.ts                     1.10   255.7±20.52ms    10.2 MB/sec     1.00   232.0±19.62ms    11.2 MB/sec
formatter/compiler.js                    1.23    146.9±8.74ms     7.1 MB/sec     1.00    119.0±9.99ms     8.8 MB/sec
formatter/d3.min.js                      1.23    112.0±8.77ms     2.3 MB/sec     1.00     91.1±0.46ms     2.9 MB/sec
formatter/dojo.js                        1.11      7.8±0.56ms     8.8 MB/sec     1.00      7.1±0.61ms     9.7 MB/sec
formatter/ios.d.ts                       1.16   175.9±17.13ms    10.6 MB/sec     1.00   151.2±15.59ms    12.3 MB/sec
formatter/jquery.min.js                  1.11     29.1±1.80ms     2.8 MB/sec     1.00     26.3±0.35ms     3.1 MB/sec
formatter/math.js                        1.09   218.4±15.09ms     3.0 MB/sec     1.00   201.2±16.86ms     3.2 MB/sec
formatter/parser.ts                      1.00      4.8±0.03ms    10.2 MB/sec     1.00      4.8±0.43ms    10.2 MB/sec
formatter/pixi.min.js                    1.22   123.7±10.55ms     3.5 MB/sec     1.00    101.4±2.12ms     4.3 MB/sec
formatter/react-dom.production.min.js    1.31     41.4±0.30ms     2.8 MB/sec     1.00     31.7±2.13ms     3.6 MB/sec
formatter/react.production.min.js        1.22  1897.4±154.67µs     3.2 MB/sec    1.00  1552.4±28.32µs     4.0 MB/sec
formatter/router.ts                      1.04      3.9±0.06ms    15.9 MB/sec     1.00      3.7±0.03ms    16.5 MB/sec
formatter/tex-chtml-full.js              1.16   285.2±24.56ms     3.2 MB/sec     1.00   246.5±19.11ms     3.7 MB/sec
formatter/three.min.js                   1.11    131.9±1.09ms     4.5 MB/sec     1.00    118.6±0.54ms     5.0 MB/sec
formatter/typescript.js                  1.16   984.6±64.70ms     9.6 MB/sec     1.00   850.6±71.32ms    11.2 MB/sec
formatter/vue.global.prod.js             1.13     47.1±4.10ms     2.6 MB/sec     1.00     41.7±3.13ms     2.9 MB/sec

Windows

group                                    comments                               flat
-----                                    --------                               ----
formatter/checker.ts                     1.29    273.3±8.10ms     8.2 MB/sec    1.00    212.3±3.44ms    10.6 MB/sec
formatter/compiler.js                    1.10    125.7±3.05ms     8.1 MB/sec    1.00    114.6±2.07ms     8.9 MB/sec
formatter/d3.min.js                      1.01    103.8±3.20ms     4.1 MB/sec    1.00    102.7±2.38ms     4.1 MB/sec
formatter/dojo.js                        1.05      6.7±0.06ms    11.1 MB/sec    1.00      6.3±0.05ms    11.6 MB/sec
formatter/ios.d.ts                       1.21    169.6±6.03ms    11.3 MB/sec    1.00    140.6±5.71ms    13.6 MB/sec
formatter/jquery.min.js                  1.00     27.8±0.32ms     4.8 MB/sec    1.00     27.9±0.27ms     4.8 MB/sec
formatter/math.js                        1.38   273.4±16.23ms     4.3 MB/sec    1.00    198.2±6.36ms     5.9 MB/sec
formatter/parser.ts                      1.13      4.7±0.14ms    10.0 MB/sec    1.00      4.2±0.05ms    11.2 MB/sec
formatter/pixi.min.js                    1.11    121.0±5.29ms     5.3 MB/sec    1.00    108.8±2.49ms     5.9 MB/sec
formatter/react-dom.production.min.js    1.05     33.4±0.51ms     5.0 MB/sec    1.00     31.9±0.55ms     5.2 MB/sec
formatter/react.production.min.js        1.07  1668.2±18.89µs     4.9 MB/sec    1.00  1559.1±16.56µs     5.3 MB/sec
formatter/router.ts                      1.11      3.7±0.11ms    16.2 MB/sec    1.00      3.3±0.05ms    18.0 MB/sec
formatter/tex-chtml-full.js              1.29   323.0±13.78ms     4.5 MB/sec    1.00    251.2±4.12ms     5.8 MB/sec
formatter/three.min.js                   1.15    143.6±7.96ms     5.6 MB/sec    1.00    125.1±2.15ms     6.4 MB/sec
formatter/typescript.js                  1.27  1072.5±97.80ms     7.9 MB/sec    1.00    846.8±5.00ms    10.0 MB/sec
formatter/vue.global.prod.js             1.06     43.5±0.85ms     4.3 MB/sec    1.00     41.0±0.47ms     4.6 MB/sec

Mac

group                                    comments                               flat
-----                                    --------                               ----
formatter/checker.ts                     1.03    227.1±2.55ms    11.4 MB/sec    1.00    219.6±5.62ms    11.8 MB/sec
formatter/compiler.js                    1.03    127.6±1.41ms     8.2 MB/sec    1.00    123.9±4.12ms     8.5 MB/sec
formatter/d3.min.js                      1.05    101.0±1.07ms     2.6 MB/sec    1.00     96.3±1.11ms     2.7 MB/sec
formatter/dojo.js                        1.07      7.0±0.08ms     9.8 MB/sec    1.00      6.6±0.08ms    10.4 MB/sec
formatter/ios.d.ts                       1.05    162.4±1.67ms    11.5 MB/sec    1.00    154.4±1.69ms    12.1 MB/sec
formatter/jquery.min.js                  1.06     28.2±0.27ms     2.9 MB/sec    1.00     26.6±0.28ms     3.1 MB/sec
formatter/math.js                        1.05    199.8±2.06ms     3.2 MB/sec    1.00    189.9±2.00ms     3.4 MB/sec
formatter/parser.ts                      1.07      4.8±0.10ms    10.1 MB/sec    1.00      4.5±0.16ms    10.8 MB/sec
formatter/pixi.min.js                    1.07    114.7±0.97ms     3.8 MB/sec    1.00    106.8±0.92ms     4.1 MB/sec
formatter/react-dom.production.min.js    1.08     34.7±0.51ms     3.3 MB/sec    1.00     32.2±0.43ms     3.6 MB/sec
formatter/react.production.min.js        1.07  1672.2±21.39µs     3.7 MB/sec    1.00  1564.1±18.02µs     3.9 MB/sec
formatter/router.ts                      1.07      3.9±0.14ms    15.8 MB/sec    1.00      3.6±0.13ms    17.0 MB/sec
formatter/tex-chtml-full.js              1.04    255.3±2.63ms     3.6 MB/sec    1.00    245.4±3.07ms     3.7 MB/sec
formatter/three.min.js                   1.02    129.8±1.36ms     4.5 MB/sec    1.00    126.7±2.13ms     4.6 MB/sec
formatter/typescript.js                  1.08    843.3±8.41ms    11.3 MB/sec    1.00    778.3±9.53ms    12.2 MB/sec
formatter/vue.global.prod.js             1.07     44.1±0.44ms     2.7 MB/sec    1.00     41.3±0.42ms     2.9 MB/sec

Allocations

The number of allocations are drastically reduced but the total allocated bytes increases. My understanding of why the total memory increases is because Vecs grow relative to their size, meaning that larger Vecs may allocate a lot of space for unused "elements".

The memory consumption should be of little concerns as these get released as soon as formatting is done.

jquery
At t-gmax:

  • bytes: 6,664,064 -> 11,038,544 ~40% increase
  • blocks: 62,138 -> 28,937 ~50% reduction

tex-chtml-full

  • bytes: 60,930,993 -> 101,799,569
  • blocks: 519,222 -> 207,094 -> 60% reduction

Test Plan

I added a few new tests for the newly introduced data structures but otherwise mainly relied on the existing tests.

@netlify
Copy link

netlify bot commented Sep 5, 2022

Deploy Preview for rometools canceled.

Name Link
🔨 Latest commit a44decc
🔍 Latest deploy log https://app.netlify.com/sites/rometools/deploys/632dad216779c50009e95d28

@MichaReiser
Copy link
Contributor Author

The performance is regressing at the moment because each group is adding another layer of GroupsElementsBuffer which starts to add a lot of overhead to every state, context, and most importantly, write_element call.

The comments refactoring should remove the need for the GroupsElementsBuffer so that we can then try again

@MichaReiser MichaReiser temporarily deployed to netlify-playground September 5, 2022 13:25 Inactive
@github-actions
Copy link

github-actions bot commented Sep 5, 2022

@MichaReiser MichaReiser temporarily deployed to netlify-playground September 16, 2022 14:28 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 16, 2022 14:58 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 16, 2022 15:54 Inactive
@github-actions
Copy link

Parser conformance results on ubuntu-latest

js/262

Test result main count This PR count Difference
Total 45879 45879 0
Passed 44939 44939 0
Failed 940 940 0
Panics 0 0 0
Coverage 97.95% 97.95% 0.00%

jsx/babel

Test result main count This PR count Difference
Total 39 39 0
Passed 36 36 0
Failed 3 3 0
Panics 0 0 0
Coverage 92.31% 92.31% 0.00%

symbols/microsoft

Test result main count This PR count Difference
Total 5946 5946 0
Passed 1621 1621 0
Failed 4325 4325 0
Panics 0 0 0
Coverage 27.26% 27.26% 0.00%

ts/babel

Test result main count This PR count Difference
Total 588 588 0
Passed 519 519 0
Failed 69 69 0
Panics 0 0 0
Coverage 88.27% 88.27% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 16257 16257 0
Passed 12395 12395 0
Failed 3862 3862 0
Panics 0 0 0
Coverage 76.24% 76.24% 0.00%

@MichaReiser MichaReiser temporarily deployed to netlify-playground September 19, 2022 12:30 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 21, 2022 07:48 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 21, 2022 08:06 Inactive
@MichaReiser MichaReiser changed the base branch from main to feat/assert-comments-formatting September 21, 2022 08:08
@@ -7,8 +7,6 @@ repository = "https://github.com/rome/tools"
description = "WebAssembly bindings to the Rome Workspace API"
license = "MIT"

[package.metadata.wasm-pack.profile.dev]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The wasm optimisations were necessary to avoid the playground from stack overflowing. This seems to be no longer necessary and I believe it comes from the fact that the Display implementation for &[FormatElement] (used in the RomeIR tab is no longer recursive.

The problem was that the old Display implementation recursed for every container (and, performance wise, created a group and soft block indent vec for every container...).

@MichaReiser MichaReiser added the A-Formatter Area: formatter label Sep 21, 2022
@MichaReiser MichaReiser added this to the 0.10.0 milestone Sep 21, 2022
@MichaReiser MichaReiser linked an issue Sep 21, 2022 that may be closed by this pull request
@MichaReiser MichaReiser marked this pull request as ready for review September 21, 2022 09:48
@MichaReiser MichaReiser requested a review from a team September 21, 2022 09:48
@MichaReiser MichaReiser force-pushed the feat/assert-comments-formatting branch from c568991 to 05aab2b Compare September 22, 2022 06:21
@MichaReiser MichaReiser force-pushed the refactor/flat-ir branch 4 times, most recently from 655ce46 to 9d19c50 Compare September 22, 2022 07:05
@MichaReiser MichaReiser force-pushed the feat/assert-comments-formatting branch from 05aab2b to 868e1c9 Compare September 22, 2022 07:05
@MichaReiser MichaReiser force-pushed the feat/assert-comments-formatting branch from 868e1c9 to a731169 Compare September 22, 2022 07:42
@calibre-analytics
Copy link

calibre-analytics bot commented Sep 22, 2022

Comparing refactor(rome_formatter): Flat IR Snapshot #9 to median since last deploy of rome.tools.

LCP? CLS? TBT?
Overall
Median across all pages and test profiles
1.75s
from 565ms
0.0
no change
152ms
from 37ms
Chrome Desktop
Chrome Desktop • Cable
1.75s
from 573ms
0.0
no change
297ms
from 227ms
iPhone, 4G LTE
iPhone 12 • 4G LTE
1.06s
from 218ms
0.0
no change
1ms
from 6ms
Motorola Moto G Power, 3G connection
Motorola Moto G Power • Regular 3G
12.7s
from 565ms
0.0
no change
152ms
from 37ms

1 page tested

 Home

Browser previews

Chrome Desktop iPhone, 4G LTE Motorola Moto G Power, 3G connection
Chrome Desktop iPhone, 4G LTE Motorola Moto G Power, 3G connection

Most significant changes

Value Budget
JS Parse & Compile
iPhone, 4G LTE
490ms
from 7ms
JS Parse & Compile
Motorola Moto G Power, 3G connection
1.55s
from 29ms
Total JavaScript Size in Bytes
Chrome Desktop
4.04 MB
from 86.8 KB
Total JavaScript Size in Bytes
iPhone, 4G LTE
4.04 MB
from 86.8 KB
Total JavaScript Size in Bytes
Motorola Moto G Power, 3G connection
4.04 MB
from 86.8 KB

28 other significant changes: JS Parse & Compile on Chrome Desktop, First Contentful Paint on Motorola Moto G Power, 3G connection, Largest Contentful Paint on Motorola Moto G Power, 3G connection, Speed Index on Motorola Moto G Power, 3G connection, Total Page Size in Bytes on Chrome Desktop, Total Page Size in Bytes on iPhone, 4G LTE, Total Page Size in Bytes on Motorola Moto G Power, 3G connection, Time to Interactive on Motorola Moto G Power, 3G connection, Number of Requests on Motorola Moto G Power, 3G connection, Number of Requests on Chrome Desktop, Number of Requests on iPhone, 4G LTE, First Contentful Paint on Chrome Desktop, First Contentful Paint on iPhone, 4G LTE, Time to Interactive on Chrome Desktop, Speed Index on iPhone, 4G LTE, Largest Contentful Paint on iPhone, 4G LTE, Time to Interactive on iPhone, 4G LTE, Speed Index on Chrome Desktop, Total Blocking Time on Motorola Moto G Power, 3G connection, Largest Contentful Paint on Chrome Desktop, Total Image Size in Bytes on Chrome Desktop, Total Image Size in Bytes on iPhone, 4G LTE, Total Image Size in Bytes on Motorola Moto G Power, 3G connection, Total HTML Size in Bytes on Chrome Desktop, Total HTML Size in Bytes on iPhone, 4G LTE, Total HTML Size in Bytes on Motorola Moto G Power, 3G connection, Lighthouse Performance Score on Motorola Moto G Power, 3G connection, Lighthouse Performance Score on Chrome Desktop

Calibre: Site dashboard | View this PR | Edit settings | View documentation

@ematipico
Copy link
Contributor

!bench_formatter

@github-actions
Copy link

Formatter Benchmark Results

group                                    main                                   pr
-----                                    ----                                   --
formatter/checker.ts                     1.08   385.8±23.67ms     6.7 MB/sec    1.00   358.6±13.14ms     7.2 MB/sec
formatter/compiler.js                    1.13    216.0±9.70ms     4.9 MB/sec    1.00    191.4±6.40ms     5.5 MB/sec
formatter/d3.min.js                      1.07   165.7±10.63ms  1620.3 KB/sec    1.00    154.2±5.65ms  1740.4 KB/sec
formatter/dojo.js                        1.14     11.2±0.57ms     6.1 MB/sec    1.00      9.8±0.39ms     7.0 MB/sec
formatter/ios.d.ts                       1.03    231.1±5.92ms     8.1 MB/sec    1.00    225.0±6.15ms     8.3 MB/sec
formatter/jquery.min.js                  1.07     42.6±1.78ms  1987.4 KB/sec    1.00     39.9±1.41ms     2.1 MB/sec
formatter/math.js                        1.04   315.2±10.61ms     2.1 MB/sec    1.00   302.1±10.24ms     2.1 MB/sec
formatter/parser.ts                      1.08      7.1±0.31ms     6.9 MB/sec    1.00      6.6±0.20ms     7.4 MB/sec
formatter/pixi.min.js                    1.05    176.7±6.44ms     2.5 MB/sec    1.00    168.0±8.38ms     2.6 MB/sec
formatter/react-dom.production.min.js    1.13     57.2±3.33ms     2.0 MB/sec    1.00     50.7±2.24ms     2.3 MB/sec
formatter/react.production.min.js        1.17      2.7±0.21ms     2.2 MB/sec    1.00      2.3±0.08ms     2.6 MB/sec
formatter/router.ts                      1.01      5.7±0.21ms    10.8 MB/sec    1.00      5.6±0.20ms    10.9 MB/sec
formatter/tex-chtml-full.js              1.03   401.0±11.96ms     2.3 MB/sec    1.00   390.3±10.37ms     2.3 MB/sec
formatter/three.min.js                   1.00    195.8±6.81ms     3.0 MB/sec    1.01    197.9±6.66ms     3.0 MB/sec
formatter/typescript.js                  1.05  1409.3±30.70ms     6.7 MB/sec    1.00  1336.6±42.75ms     7.1 MB/sec
formatter/vue.global.prod.js             1.19     74.1±4.92ms  1665.2 KB/sec    1.00     62.5±2.67ms  1974.4 KB/sec

@ematipico
Copy link
Contributor

We use the concept of "signal" inside the analyzer, which has its own semantics and it plays will that infrastructure and the actual tool, the transformer. Here, it seems that the concept has a different semantic.

My main concern is the overlap of two concepts that have different meanings but same name. It causes confusion to me.

A suggestion, maybe could use "Event" instead - just a suggestion and it's arbitrary -, we have an example of an external crate where it's used: https://github.com/rome/tools/blob/main/xtask/lintdoc/src/main.rs#L221-L243

They look similar to me.

@MichaReiser
Copy link
Contributor Author

MichaReiser commented Sep 22, 2022

A suggestion, maybe could use "Event" instead - just a suggestion and it's arbitrary -, we have an example of an external crate where it's used: main/xtask/lintdoc/src/main.rs#L221-L243

I'm conflicted on Event because it represents the document structure and not that something happened (I guess signal has the same flaw). But I saw that the example uses the term Tag which is similar to what the signals represent. Each signal has a start/end tag (maybe rename it to open and close).

The one downside I see is that FormatElement contains the name Element which, in HTML terminology, is a tag. This could be solved by renaming it to FormatItem but I don't think it's worth it

What do you think of the name Tag?

crates/rome_formatter/src/formatter.rs Show resolved Hide resolved
@@ -458,6 +579,7 @@ impl std::fmt::Display for FormatError {
fmt,
"formatting range {input:?} is larger than syntax tree {tree:?}"
),
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please open an issue https://github.com/rome/tools/issues."),
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
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please open an issue https://github.com/rome/tools/issues."),
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please report it if necessary."),

The phrase is inline with the other diagnostics. It make sense to show the link, but it should be printed using the Hyperlink markup which handles links inside the terminal, making it clickable.

Maybe @leops has a suggestion on how to do that (maybe doing it automatically for Fatal errors?)

@ematipico
Copy link
Contributor

What do you think of the name Tag?

Yeah that works for me

@MichaReiser MichaReiser force-pushed the feat/assert-comments-formatting branch from 9f566e7 to 88a9d4f Compare September 23, 2022 06:51
Base automatically changed from feat/assert-comments-formatting to main September 23, 2022 11:36
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 23, 2022 11:39 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 23, 2022 12:04 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 23, 2022 12:18 Inactive
@MichaReiser MichaReiser temporarily deployed to netlify-playground September 23, 2022 12:57 Inactive
@MichaReiser MichaReiser merged commit 1022663 into main Sep 23, 2022
@MichaReiser MichaReiser deleted the refactor/flat-ir branch September 23, 2022 13:12
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A-Formatter Area: formatter
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Formatter: Performance [Stretch]
3 participants