leodevbro / vscode-blockman

VSCode extension to highlight nested code blocks
https://github.com/leodevbro/vscode-blockman
MIT License
345 stars 16 forks source link

Fantastic extension needs performance optimization #50

Closed noga-dev closed 2 years ago

noga-dev commented 2 years ago

Issue Type: Performance Issue

Love this extension. Helps me visualize my position in code, especially in deeply nested widgets. But it's lagging even on my high end device. Even at N6 it's barely usable. Also I suspect it might have a memory leak, as it keeps getting worse and gets better after a a reload.

Extension version: 1.2.16 VS Code version: Code - Insiders 1.62.0-insider (4bbec283c36a51cf80f9b77c7a81c140a76a363b, 2021-11-02T08:19:40.220Z) OS version: Windows_NT x64 10.0.22000 Restricted Mode: No

System Info |Item|Value| |---|---| |CPUs|AMD Ryzen 9 4900H with Radeon Graphics (16 x 3294)| |GPU Status|2d_canvas: enabled
gpu_compositing: enabled
multiple_raster_threads: enabled_on
oop_rasterization: enabled
opengl: enabled_on
rasterization: enabled
skia_renderer: enabled_on
video_decode: enabled
vulkan: disabled_off
webgl: enabled
webgl2: enabled| |Load (avg)|undefined| |Memory (System)|31.42GB (12.52GB free)| |Process Argv|--crash-reporter-id 76213229-3f45-4857-b470-1778a95699b5| |Screen Reader|no| |VM|0%|
Process Info ``` CPU % Mem MB PID Process 0 137 3956 code-insiders main 0 24 9636 crashpad-handler 4 217 13612 window (series_list_sort_section.dart - banagher - Visual Studio Code - Insiders) 10 256 11588 extensionHost 0 5 19536 C:\WINDOWS\system32\cmd.exe /d /s /c ""C:\flutter\bin\cache\dart-sdk\bin\dart.exe" "C:\flutter\bin\cache\dart-sdk\bin\snapshots\analysis_server.dart.snapshot" "--lsp" "--client-id=VS-Code" "--client-version=3.28.0"" 0 11 3876 console-window-host (Windows internal process) 12 89 25268 "C:\flutter\bin\cache\dart-sdk\bin\dart.exe" "C:\flutter\bin\cache\dart-sdk\bin\snapshots\analysis_server.dart.snapshot" "--lsp" "--client-id=VS-Code" "--client-version=3.28.0" 0 6 29944 C:\WINDOWS\system32\cmd.exe /d /s /c ""C:\flutter\bin\flutter.bat" "daemon"" 0 11 30516 console-window-host (Windows internal process) 3 123 30568 "C:\flutter\bin\cache\dart-sdk\bin\dart.exe" --disable-dart-dev --packages="C:\flutter\packages\flutter_tools\.packages" "C:\flutter\bin\cache\flutter_tools.snapshot" "daemon" 0 5 30804 C:\WINDOWS\system32\cmd.exe /d /s /c ""c:\Users\Agone\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app\node_modules.asar.unpacked\vscode-ripgrep\bin\rg.exe" --no-messages --vimgrep -H --column --line-number --color never --max-columns=1000 --no-config -f "c:\Users\Agone\AppData\Roaming\Code - Insiders\User\workspaceStorage\33938f9b144b76f96ab13177c795021d\Gruntfuggly.todo-tree\2hbwjcffks.txt" -g "!**/node_modules" -g "!**/banagher/windows/**/*" -g "!c:\Users\Agone\Source\work\comikey\banagher\lib\helpers\platform.dart" "c:\Users\Agone\Source\work\comikey\banagher"" 0 11 30204 console-window-host (Windows internal process) 0 9 32232 "c:\Users\Agone\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app\node_modules.asar.unpacked\vscode-ripgrep\bin\rg.exe" --no-messages --vimgrep -H --column --line-number --color never --max-columns=1000 --no-config -f "c:\Users\Agone\AppData\Roaming\Code - Insiders\User\workspaceStorage\33938f9b144b76f96ab13177c795021d\Gruntfuggly.todo-tree\2hbwjcffks.txt" -g "!**/node_modules" -g "!**/banagher/windows/**/*" -g "!c:\Users\Agone\Source\work\comikey\banagher\lib\helpers\platform.dart" "c:\Users\Agone\Source\work\comikey\banagher" 0 6 32092 "C:\Program Files\Git\cmd\git.exe" for-each-ref --format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track) refs/heads/dev refs/remotes/dev 0 11 28588 console-window-host (Windows internal process) 0 0 30500 1 469 15708 gpu-process 0 39 25308 utility 7 139 30700 shared-process 0 68 13304 ptyHost 0 7 14368 console-window-host (Windows internal process) 0 51 26732 "C:\Program Files\PowerShell\7\pwsh.exe" 0 88 30924 watcherServiceParcelSharedProcess 0 92 31528 window (Issue Reporter) 0 7 32724 "C:\Program Files\Google\Drive File Stream\52.0.6.0\crashpad_handler.exe" --database=C:\Users\Agone\AppData\Local\Google\DriveFS\Crashpad --url=https://clients2.google.com/cr/report "--annotation=application=Code - Insiders.exe" --annotation=prod=DriveFS --annotation=ver=52.0.6.0 --initial-client-data=0x15b0,0x15a4,0x1574,0x1594,0x1558,0x7ffce569f820,0x7ffce569f830,0x7ffce569f840 ```
Workspace Info ``` | Window (series_list_sort_section.dart - banagher - Visual Studio Code - Insiders) | Folder (banagher): 10689 files | File types: xml(2133) json(1217) flat(1056) svg(308) class(294) | jar(219) png(178) dart(156) txt(126) len(123) | Conf files: cmake(12) sln(6) github-actions(1) launch.json(1) | tasks.json(1) | Launch Configs: dart(4); ```
leodevbro commented 2 years ago

Thanks for the feedback. Actually also I find Blockman slow generally, but not as slow as unusable. It was like 20 times slower initially, but I implemented as many optimization algorithms as I could. Currently I'm also trying to find another optimization tricks. Maybe you or anyone interested can help me with some new ideas.

Also, can you provide more specifically, how Blockman seems slow? For example, when I am typing, sometimes the editor freezes for half a second, because Blockman is trying to re-render blocks. And when I switch between file tabs, VSCode API automatically clears the blocks if the file is no longer visible on screen. But it does not clear them from memory. Blockman finds those trash blocks and deletes them, but if you jump between tabs too quickly, then if I am not wrong, the VSCode API behaves abnormal and the extensions lose access to the trash blocks, so it becomes impossible to clean the memory in such cases. But Blockman detects such events and now I'm going to implement a code that will make a clean restart of Blockman 3 minutes after such event, so the memory will not get too leaked (I hope so).

There is also one important thing: The file analyzation process is very instant, even if the file has 5 thousand lines, but it takes time to render/rerender blocks based on the analysis. And the rendering function is setDecorations which is part of the VSCode API, and this function behaves slow and I cannot change the source code of the VSCode API.

Well, the indent-rainbow extension also uses setDecorations function, but it renders just a solid color. Blockman renders complex HTML element like this:

const lineDecoration = vscode.window.createTextEditorDecorationType({
        before: {
            // rangeBehavior: 1,

            contentText: ``,
            textDecoration: `;box-sizing: content-box !important;
                              ${borderCss}

                              border-radius: ${borderRadiusCss};

                              width: calc((${
                                  boxRightEdge - boxLeftEdge
                              } * (1ch + ${
                glo.letterSpacing
            }px)) - ${leftInc}px);
                              height: ${specificHeight}px;
                              position: absolute;
                              z-index: ${zIndex};
                              top: ${top}px;
                              left: calc((${boxLeftEdge} * (1ch + ${
                glo.letterSpacing
            }px)) + ${leftInc - borderSize}px);
                              ${backgroundAndBorder}
                              `,
            // padding: 100px;
        },
    } as vscode.DecorationRenderOptions);

and such HTML div must be rendered on each line, actually several divs on each line, and mostly rerender on each change event of the text.


Also you can play arount the optimization settings of Blockman. For example

set 2 or 3 seconds in: N28 Time To Wait Before Rerender After Last Change Event default is 1.2 seconds. Well, more time means less frequent updates, but will have less CPU load.

set 2 or maybe 5 or 10 in: N31 Render Increment Before And After Visible Range default is 22. With less number, scrolling becomes less comfortable but will have less CPU load.

noga-dev commented 2 years ago

I may have exaggerated my inconveniences. When I say that it's lagging, I mean that it's not able to keep up with me. ADHD things, I suppose. I hop around a lot. The files I use currently are never more than 3 digit lines long. I constantly see the UI freeze up for anywhere between a micro second and half a second. And it gets worse the longer my session is due to the aforementioned memory leak.

I'll keep trying to tweak around with the settings, but I should note that this extension is hard to live without in my environment (Flutter). And ideally it would be great to have the max depth at 20 if not more. But even at ground level it's super useful. But currently I have to keep it at 3 or else the lags start to get unbearable.

Btw I also performed extension bisect and Blockman was the only outlier. And notice the 2s startup time.

SS ![image](https://user-images.githubusercontent.com/2067558/140101011-0263f385-22bd-4ba2-89e9-bc82f6dd4279.png)

and such HTML div must be rendered on each line, actually several divs on each line, and mostly rerender on each change event of the text.

No debouncer?

leodevbro commented 2 years ago

No debouncer?

No point of using debouncer, because we need each div for each line, we don't want to lose any of them, otherwise blocks will not appear as full blocks.

Yesterday I investigated deeper into the performance, and found out that rendering is not a significant factor of slowing down. In fact, rendering is fine. I'm also going to make rendering even faster and with better handling of memory leaking. It turns out that finding python indentations and finding brackets (curly, square, round) is the main performance degrader. Finding tags (HTML, XML, TSX) is very fast even for 20k lines, ~also finding Python indentations is very fast~ (Turns out the analyzer function of Python is very slow, it takes about 10 seconds for a 10,000 line file. An acceptable time is maybe under 2 seconds, so it will be under 0.2 seconds for a 1,000 line file). Finding brackets is also very slow but about 5 times faster than finding Python indentations.

The VSCode team implemented native bracket finding feature in VSCode, it works very fast, and maybe soon they will give extensions access to the API of getting the locations of those brackets.

noga-dev commented 2 years ago

I see. Looks like you're on top of things, so I'll close this issue and do what I can to mitigate things on my end (using your suggestions above did yield some positive results).

leodevbro commented 2 years ago

Good news, I discovered a great optimization trick in VSCode API. Basically, I can reuse most (like 90%) of the line blocks for rendering (In version 1.2.16, new HTML div is being created for each line, so it's very slow), so in new version, only about 10% of the HTML divs will stress the CPU. Already tested it successfully. I'm not totally sure, but I am like 95% sure that the final code will be much faster. I'm going to release the new version soon, maybe tomorrow, or maybe the day after tomorrow.

noga-dev commented 2 years ago

Good news, I discovered a great optimization trick in VSCode API. Basically, I can reuse most (like 90%) of the line blocks for rendering (In version 1.2.16, new HTML div is being created for each line, so it's very slow), so in new version, only about 10% of the HTML divs will stress the CPU. Already tested it successfully. I'm not totally sure, but I am like 95% sure that the final code will be much faster. I'm going to release the new version soon, maybe tomorrow, or maybe the day after tomorrow.

My god, you beautiful soul. Watching every move, sir.

leodevbro commented 2 years ago

Did some more deep investigations, the rendering of HTML blocks now seems like 2-3 times faster with the new optimization trick. But turns out that the HTML rendering always was under 0.07 seconds (with default Blockman settings), and so it is not as much a performance problem as the analyze (finding brackets, finding tags, finding python indentations) process.

But even though the speed gain is small with the new rendering optimization, I think it will help the entire code to be more stable, less buggy and less lagging, because the new rendering optimization made the entire code simpler and much smaller.

I will release new version of Blockman these days.

. . .

So, I did some performance tests:

PC: 32 GB RAM, AMD Ryzen 7 2700X 8-Core CPU 3.70 GHz, Windows 10 64bit OS.

Blockman: N22 Analyze Curly Brackets = true
Blockman: N23 Analyze Square Brackets = true
Blockman: N24 Analyze Round Brackets = true
Blockman: N25 Analyze Tags = true
Blockman: N26 Analyze Indent Dedent Tokens = true

Blockman: N31 Render Increment Before And After Visible Range = 30

Measured with console.time() and console.timeEnd()

Time results are written as approximate average.

Files are tested with 10,000 lines and 1,000 lines.



To be clear:
In case of Blockman, the word 'analyze' means parsing a file and saving the structural data of its blocks as a JavaScript object.
And, the word 'render' means rendering HTML divs using the structural data received from the analyzer function.

With Blockman 1.2.16

One render takes about 0.05 seconds.
Analyzer (finding brackets, tags, python indentations) function is the same as in Blockman 1.3.0 (not yet released).



With Blockman 1.3.0 (not yet released)

One render takes about 0.02 seconds.

Yaml file:
    analyze: 0.70 seconds (10,000 lines), 0.06 seconds (1,000 lines)

Dart file:
    analyze: 0.90 seconds (10,000 lines), 0.09 seconds (1,000 lines)

CSharp file:
    analyze: 1.40 seconds (10,000 lines), 0.15 seconds (1,000 lines)

TSX file:
    analyze: 1.60 seconds (10,000 lines), 0.17 seconds (1,000 lines)

JavaScript file:
    analyze: 2.70 seconds (10,000 lines), 0.28 seconds (1,000 lines)

Python file:
   analyze: 10.70 seconds (10,000 lines), 0.86 seconds (1,000 lines)


As you can see, the analyzer of Python turns out to be the slowest. But I guess all the others are fine, because it's rare to have a file with more than 2,000 lines, and they seem kinda fine for a 1,000-2,000 line file. And Python also seems fine for a 500-1,000 line file.

So, for the future, I will continue researching about the parsers of Python, JavaScript, TSX, CSharp, Yaml and more languages. The analyzer function of Blockman is using many parser/tokenizer libraries, I cannot think of making them myself, because it takes super high knowledge and maybe months to build a good parser of any language.

So, maybe these parser/tokenizer libraries have some kind of options to optimize the parsing/tokenizing process.

noga-dev commented 2 years ago

It's interesting that brackets heavy langs like C# and TSX are the mean, while indentation heavy ones like Python and Yaml have such contradictory results.

And I can't help but notice that Dart (my primary language, which is particularly brackets heavy) is missing from your test samples.

Would be happy to help you beta test the unreleased version.

leodevbro commented 2 years ago

notice that Dart (my primary language, which is particularly brackets heavy) is missing from your test samples

I updated the test samples with this:

Dart file:
    analyze: 0.90 seconds (10,000 lines), 0.09 seconds (1,000 lines)


indentation heavy ones like Python and Yaml have such contradictory results

Maybe because Yaml is not a programming language, it's just an object structure language like JSON, so it does not have many features to parse. But Python has many many programming features, but 10 seconds for 10,000 lines is still too much I guess. Maybe the parser of Python checks some extra things too, which are not needed much in a real work Python environment, so maybe I can disable them if they exist.

noga-dev commented 2 years ago

Maybe because Yaml is not a programming language, it's just an object structure language like JSON, so it does not have many features to parse. But Python has many many programming features, but 10 seconds for 10,000 lines is still too much I guess. Maybe the parser of Python checks some extra things too, which are not needed much in a real work Python environment, so maybe I can disable them if they exist.

Makes sense.

I updated the test samples with this:

Dart file:
    analyze: 0.90 seconds (10,000 lines), 0.09 seconds (1,000 lines)

This is a surprisingly good result. Maybe it would look different for a Flutter one? The level of nesting in dart using Flutter is much different from vanilla dart.

leodevbro commented 2 years ago

You can upload any kind of code in Github Gist and send the link here, and I will test it. Even if the code is only 100-200 lines, I can just copy paste many times so it becomes 10,000 lines.

noga-dev commented 2 years ago

I made a 10k syntactically valid but ugly copy-pasted Flutter code from Vandad's latest issue.

Here: https://gist.github.com/Agondev/c1f5315c64af0a0119b9b594eb5d6aca

But also interestingly, while making it, my vscode etensions manager crashed, and led to this cpuprofile file creation: https://drive.google.com/file/d/1BhdHm2WbV9xvzf4L80xCRlls76nmgrK8/view?usp=sharing

Here's an interesting snippet from it:

image

leodevbro commented 2 years ago

With that file (Dart, 10,000 lines, extremely deeply nested), analyze time is about 1.10 seconds.

With max-depth 20 render time is about 0.03 seconds.

With unlimited max-depth render time is about 0.06 seconds.

The file is very deep nested, it's like 100 level depth, even without Blockman, it feels slow to work with such files. Does anyone really work with such deep level nesting in real world working environment?

noga-dev commented 2 years ago

Oh no, It's intentionally exaggerated with nesting. Usually the nesting is limited to like 10-20 deep, unless some junior dev is feeling extra courageous.

Here's an example of a current real world file with nesting as deep as I let it get before I break it into components: image

So roughly 30 tabs, max. Which in this case is 23 brackets deep.

leodevbro commented 2 years ago

I just released the new version (1.3.0) of Blockman. I have a big expectation that now it is more stable, more performant, less laggy. I will continue researching the speed optimizations of parsing process, especially for Python. This issue (N50) can stay open before some more large scale optimizations happen.

[1.3.0] - 2021-11-10

noga-dev commented 2 years ago

Awesome! I'll be testing it in the coming days.

noga-dev commented 2 years ago

@leodevbro So I've been testing it for the past ~6 hours. I should note that right after your last update I installed it, changed some settings, closed the IDE and haven't touched it for a day. Now that I've played around with it for hours, I just checked the options again didn't notice or forgot that I set N03 to 20. So this whole time it was set to max value and hasn't stuttered at all, which means you must have also fixed the memory leak issue because it should have started stuttering, but hasn't.

image

Startup time went down from ~2-2.5 s to 250 ms. That's up to 90% reduction!

N28 was set to 3 and N30 to 10, per your suggestion in the 1st reply. Will attempt to revert those back to default now for further testing.

Overall, preach! Methinks this issue can already by safely closed, sir.

leodevbro commented 2 years ago

Very cool, for me, the startup time was 921ms, but I guess it's not a big deal even if it is 3 seconds or more. The big deal is how it performs during working process. I'm glad it performs better in your environment. I suggest to test it for several days more, and if we notice no degradation of speed, then we can close this issue.