Hi, I'm a developer on VS Code and wanted to clarify that there was actually a logical memory leak in there.
The problem is that I tried to reproduce it back in October while running from sources, and I could not reproduce any leak. I believed the issue is about the memory usage reported by the OS vs the memory usage reported by the logical JS Heap Snapshot, which is a common topic for garbage collected runtimes, such as JS / v8. That is why we believed the issue is about the v8 VM not freeing memory back to the OS when we freed memory logically.
But it turns out I tried the steps again just now, and there is a real memory leak in there! It only reproduces when running a built version of VS Code, such as VS Code Insiders or VS Code Stable. The built versions of VS Code offer file type extension recommendations, for example you open a `.php` file and a recommendation comes in to point you to the PHP extension. When running out of sources, those recommendations do not show up and the entire code responsible for those does not execute.
Thank you for bringing this up to my attention and I'm sorry for misunderstanding the initial issue.
Hovering over the activation time will show the reason the extension is active. e.g. "Activated because you opened a javascript file", "Activated on start-up", etc.
VS Code is architected in a way where extensions are not eagerly activated by default. Each extension can declare a list of activation events, such as e.g. opening a file of a certain language, invoking a specific command, starting debugging, etc. See for example the quite long list of activation events for our built-in TypeScript extension [1].
However, we also offer an activation event called ' * ', which means an extension can ask to be activated on startup. Some over-eager extensions might be using ' * ' to start up as soon as you open a VS Code window. You can find those extensions at any time using F1 > Developer: Show Running Extensions which will show the subset of extensions running at any time, and if they were activated on startup or not.
Moreover, that view can guide you into profiling the extension host and can help you easily figure out if any extension is consuming extensive CPU. This was added quite recently [2].
When we started the project, we did write tokenizers by hand. I mention that in the blog post. You can write some very fast tokenizers by hand, even in JavaScript. Of course they won't be as fast as hand written tokenizers in C, but you'd be surprised how well the code of a hand written tokenizer in JavaScript can be optimized by a JS engine, at least I was :). IR Hydra 2 is a great tool to visualise v8's IR representation of JS code [1]. It is a shame it is not built into the Chrome Dev Tools.
In the end, we simply could not write tokenizers for all languages by hand. And our users wanted to take their themes with them when switching to VS Code. That's why we added support for TM grammars and TM themes, and in hindsight I still consider it to be a very smart decision.
I hate slowness and inefficiency too, that's why I try to make the editor as fast as possible :), but at least in this case, it is not the dynamic nature of JS to blame, but rather the nature of TM grammars. TM grammars consist of rules that have regular expressions, which need to be constantly evaluated; and in order to implement a correct TM grammar interpreter, you must evaluate them.
I've looked in the past for optimization opportunities in the C land (mostly through better caching), which yielded quite nice results [1][2]. I would love if you'd want to take a look too.
At this point, in tokenization, 90% of the time is spent in C, matching regular expressions in oniguruma. More precisely, regular expressions are executed 3,933,859 times to tokenize checker.ts -- the 1.18MB file. That is with some very good caching in node-oniguruma and it just speaks to the inefficiency of the TM grammars regex based design, more than anything else.
It is definitely possible to write faster tokenizers, especially when writing them by hand (even in JS), see for example the Monaco Editor[3] where we use the TypeScript compiler's lexer as a tokenizer.
At least in this case, inefficiencies are not caused by our runtime.
Yes, those comparisons at the end show differences in rendering caused by the "approximations" used prior to VS Code 1.9. They were all caused by the difference between the ranking rules of CSS selectors and the ranking rules of TM scope selectors
- all the regular expressions in TM grammars are based on oniguruma, a regular expression library written in C.
- the only way to interpret the grammars and get anywhere near original fidelity is to use the exact same regular expression library (with its custom syntax constructs)
in VSCode, our runtime is node.js and we can use a node native module that exposes the library to JavaScript
- in the Monaco Editor, we are constrained to a browser environment where we cannot do anything similar
- we have experimented with Emscripten to compile the C library to asm.js, but performance was very poor even in Firefox (10x slower) and extremely poor in Chrome (100x slower).
- we can revisit this once WebAssembly gets traction in the major browsers, but we will still need to consider the browser matrix we support. i.e. if we support IE11 and only Edge will add WebAssembly support, what will the experience be in IE11, etc.
I've actually never tried to compute a diff and apply it inside a line, maybe I'll try it tomorrow :). A line is basically a list of spans, each having a certain class name and a certain text content. I've just always assumed that iterating over the previous spans, adjusting their class names, adjusting their text content, appending new spans or removing extra left overs would be slower than a big fat innerHTML call (given that each dom read/write access leaves the JS VM and I always thought there's a certain penalty associated with each dom call). But I will definitely try it out!
[It might not be the best method, but it was guided by measuring]:
* if there is no overlap between frames (e.g. you jump to a completely different location), the whole thing does a single innerHTML call
* otherwise:
* all the old lines that leave the viewport are removed via multiple domNode.removeChild
* all the new lines that enter the viewport are added via a single domNode.insertAdjacentHTML
* all the old lines that have changed are parsed in one single off-dom innerHTML and then cherry picked via multiple domNode.replaceChild
That is what I could come up with in my attempts to minimize the count of dom calls and not pay for reparsing/repainting the entire viewport on each frame. Maybe there are better ways?
If I remember correctly, we ended up not using native browser scrolling for multiple reasons:
* [if we would not set scrollTop ourselves] the browser would just happily jump to a certain scrollTop, painting nothing (white), then we'd get an `onscroll` and we'd be able to paint the lines. But you'd always get this white flash.
* if we would set scrollTop ourselves:
* AFAIK setting the scrollTop causes a stop the world sort of synchronous layout - I don't know why
* We wanted to have an overview ruler that sits inside the scrollbar and highlights things (find matches, diffs, etc.)
* IE has or had a limit around 2.5M px. That meant we would have had to do something special anyways around 80.000 lines @ 19px line height
Some anecdotal evidence I got that making less calls with larger chunks of data might be better was when I was investigating why creating an editor buffer was slow for very large files (100k+ lines). One of the first things the model (buffer) code did was to split the text into an array of lines.
I implemented this as any sane person would, with a nice for loop, iterating over the string, grabbing the character code at each offset and checking if it was \r or \n or a \r followed by a \n. I would then remember the last cut off index and do a simple substring to extract each line, building an array of lines. I thought that must be the best way one could possibly do this (I don't know a better way than a for loop even in C++).
If I remember correctly, that was taking 50ms in some browser for a pretty large string. I replaced that simple for loop with a lame split with a regex! - /\r\n|\r|\n/ - and the time dropped to 3ms. I can only think that looping in C++ must be a lot better than looping in JS [here's the code today - https://github.com/Microsoft/vscode/blob/master/src/vs/edito...]
We don't show leading/trailing whitespace diffs unless the diff consists only of leading/trailing whitespace changes.
This is sort of what we do when diffing:
* when we need to compare two buffers, we represent them as two arrays of lines
* we then proceed to trim() each line in both arrays
* we then use a greedy optimization where the first N and the last M lines that are equal (post trimming) in both arrays are dropped from further computation
* we then run a LCS algorithm over the remaining lines to find the diffs
It is important to note that the same two arrays of lines can have multiple equal longest common substrings. This method [1] could get some love and could try to recover in some of these cases.
Implementing touch selection on a widget that is not native is not trivial. In the editor, when you select something, what you see painted is not really the browser (native) selection. It is simply a bunch of divs painted in such a way that they look like a selection. To add touch selection we would pretty much need to implement it from scratch.
If you have the chance and the time, I would very much appreciate a PR to fix some of the input handling quirks you're seeing on iOS. We are not experts in everything, and I have come to appreciate the amazing world of OSS :). e.g. Recently, we've gotten an amazing PR [1] that fixes a lot of the input handling for CJK languages, which I would have had no chance to fix by myself.
We are definitely cheating there - we use the same colorizer for C and C++. I don't remember why we define two different languages that point to the same colorizer.
Please help us out and contribute a PR. Here[1] is where we register the C language and point to the cpp colorizer. And here[2] is the cpp colorizer. Please go ahead and try the C++ colorizer out at the Monarch playground[3] and edit it to make it colorize only C keywords, etc. Here[4] are the steps for adding a new colorizer.
I'm working on the editor since almost 5 years now. Phew, time flies.
There is no silver bullet, we mostly try to keep all computations limited to the viewport size (if you have 20 lines visible, then typing, colorizing, painting a frame, etc. should all end up being computed with loops covering those 20 lines and not the entire buffer size).
We also use extensively the profilers, and most recently (last month) I learned about this great tool called IR Hydra[1]. The gains from eliminating bailouts in the hot code paths are probably too small to notice (5-10% per render), but I like to think that everything adds up.
We use translate3d for scrolling (except in Firefox which has a known bug[2]) and that brings down the browser painting times considerably when scrolling.
I've also found insertAdjacentHTML to be the fastest way to create dom nodes (from big fat strings) across all browsers.
Sort of silly to mention, but we use binary search a lot :).
The problem is that I tried to reproduce it back in October while running from sources, and I could not reproduce any leak. I believed the issue is about the memory usage reported by the OS vs the memory usage reported by the logical JS Heap Snapshot, which is a common topic for garbage collected runtimes, such as JS / v8. That is why we believed the issue is about the v8 VM not freeing memory back to the OS when we freed memory logically.
But it turns out I tried the steps again just now, and there is a real memory leak in there! It only reproduces when running a built version of VS Code, such as VS Code Insiders or VS Code Stable. The built versions of VS Code offer file type extension recommendations, for example you open a `.php` file and a recommendation comes in to point you to the PHP extension. When running out of sources, those recommendations do not show up and the entire code responsible for those does not execute.
Thank you for bringing this up to my attention and I'm sorry for misunderstanding the initial issue.