구문 강조의 최적화
2017년 2월 8일 - Alexandru Dima
Visual Studio Code 버전 1.9에는 저희가 작업해 온 멋진 성능 개선이 포함되어 있으며, 그 이야기를 들려드리고자 합니다.
요약 VS Code 1.9에서는 TextMate 테마가 작성자의 의도대로 더 많이 표시되며, 더 빠르고 메모리 사용량이 줄어듭니다.
구문 강조
구문 강조는 일반적으로 두 단계로 이루어집니다. 소스 코드에 토큰이 할당되고, 테마가 이를 대상으로 색상을 할당하면, 소스 코드가 색상으로 렌더링됩니다. 텍스트 편집기를 코드 편집기로 만드는 이 기능입니다.
VS Code(및 Monaco Editor)의 토큰화는 한 번에 한 줄씩, 위에서 아래로 단일 패스로 실행됩니다. 토크나이저는 토큰화된 줄의 끝에서 일부 상태를 저장할 수 있으며, 이는 다음 줄을 토큰화할 때 다시 전달됩니다. 이는 TextMate 문법을 포함한 많은 토큰화 엔진에서 사용되는 기술로, 사용자가 편집할 때 편집기가 소수의 줄만 다시 토큰화할 수 있도록 합니다.
대부분의 경우, 줄에 타이핑하는 것은 해당 줄만 다시 토큰화하는 결과를 낳습니다. 토크나이저가 동일한 종료 상태를 반환하고 편집기가 다음 줄에 새 토큰이 추가되지 않는다고 가정할 수 있기 때문입니다.
더 드물게, 줄에 타이핑하는 것은 현재 줄과 그 아래 줄의 일부를 다시 토큰화/다시 칠하는 결과를 낳습니다(동일한 종료 상태가 발견될 때까지).
과거에 토큰을 표현하는 방식
VS Code의 편집기 코드는 VS Code가 존재하기 훨씬 전에 작성되었습니다. Internet Explorer의 F12 도구를 포함한 다양한 Microsoft 프로젝트에서 Monaco Editor 형태로 배포되었습니다. 저희의 요구 사항 중 하나는 메모리 사용량을 줄이는 것이었습니다.
과거에는 수작업으로 토크나이저를 작성했습니다(오늘날에도 브라우저에서 TextMate 문법을 해석하는 실행 가능한 방법은 없지만, 그것은 다른 이야기입니다). 아래 줄의 경우, 수작업으로 작성된 토크나이저에서 다음과 같은 토큰을 얻었습니다.
tokens = [
{ startIndex: 0, type: 'keyword.js' },
{ startIndex: 8, type: '' },
{ startIndex: 9, type: 'identifier.js' },
{ startIndex: 11, type: 'delimiter.paren.js' },
{ startIndex: 12, type: 'delimiter.paren.js' },
{ startIndex: 13, type: '' },
{ startIndex: 14, type: 'delimiter.curly.js' }
];
이 토큰 배열을 유지하는 데 Chrome에서 648바이트가 소요되므로, 이러한 객체를 저장하는 것은 메모리 측면에서 매우 비쌉니다(각 객체 인스턴스는 프로토타입, 속성 목록 등으로의 포인터를 위한 공간을 예약해야 합니다). 현재 컴퓨터에는 RAM이 많지만, 15자 줄에 648바이트를 저장하는 것은 용납할 수 없습니다.
따라서 당시에는 토큰을 저장하기 위한 이진 형식을 고안했으며, 이 형식은 VS Code 1.8까지 사용되었습니다. 중복되는 토큰 유형이 있을 것이므로, 각 파일별로 별도의 맵에 수집했습니다. 다음과 같은 작업을 수행했습니다.
// 0 1 2 3 4
map = ['', 'keyword.js', 'identifier.js', 'delimiter.paren.js', 'delimiter.curly.js'];
tokens = [
{ startIndex: 0, type: 1 },
{ startIndex: 8, type: 0 },
{ startIndex: 9, type: 2 },
{ startIndex: 11, type: 3 },
{ startIndex: 12, type: 3 },
{ startIndex: 13, type: 0 },
{ startIndex: 14, type: 4 }
];
그런 다음 `startIndex`(32비트)와 `type`(16비트)을 JavaScript 숫자가 가진 53개의 맨티사 비트 중 48비트에 인코딩했습니다. 그러면 토큰 배열은 다음과 같으며, 맵 배열은 전체 파일에 재사용됩니다.
tokens = [
// type startIndex
4294967296, // 0000000000000001 00000000000000000000000000000000
8, // 0000000000000000 00000000000000000000000000001000
8589934601, // 0000000000000010 00000000000000000000000000001001
12884901899, // 0000000000000011 00000000000000000000000000001011
12884901900, // 0000000000000011 00000000000000000000000000001100
13, // 0000000000000000 00000000000000000000000000001101
17179869198 // 0000000000000100 00000000000000000000000000001110
];
이 토큰 배열을 유지하는 데 Chrome에서 104바이트가 소요됩니다. 요소 자체는 56바이트(7 x 64비트 숫자)만 소요되어야 하며, 나머지는 v8이 배열과 함께 다른 메타데이터를 저장하거나, 2의 거듭제곱으로 백킹 스토어를 할당하는 것으로 설명될 수 있습니다. 그러나 메모리 절감 효과는 분명하며, 줄당 토큰 수가 많을수록 개선됩니다. 저희는 이 접근 방식에 만족했으며 그 이후로 이 표현을 사용해 왔습니다.
참고: 토큰을 저장하는 더 간결한 방법이 있을 수 있지만, 이진 검색 가능한 선형 형식으로 유지하는 것이 메모리 사용량과 액세스 성능 측면에서 최적의 절충점을 제공합니다.
토큰 <-> 테마 일치
스타일링을 CSS에 맡기는 것과 같은 브라우저 모범 사례를 따르는 것이 좋다고 생각했습니다. 따라서 위 줄을 렌더링할 때, `map`을 사용하여 이진 토큰을 디코딩한 다음 다음과 같이 토큰 유형을 사용하여 렌더링했습니다.
<span class="token keyword js">function</span>
<span class="token"> </span>
<span class="token identifier js">f1</span>
<span class="token delimiter paren js">(</span>
<span class="token delimiter paren js">)</span>
<span class="token"> </span>
<span class="token delimiter curly js">{</span>
테마를 CSS로 작성했습니다(예: Visual Studio 테마).
...
.monaco-editor.vs .token.delimiter { color: #000000; }
.monaco-editor.vs .token.keyword { color: #0000FF; }
.monaco-editor.vs .token.keyword.flow { color: #AF00DB; }
...
결과는 꽤 좋았습니다. 클래스 이름을 바꾸면 즉시 편집기에 새 테마가 적용되었습니다.
TextMate 문법
VS Code 출시 당시에는 웹 언어를 중심으로 약 10개의 수작업 토크나이저가 있었는데, 이는 범용 데스크톱 코드 편집기에는 충분하지 않았을 것입니다. TextMate 문법은 토큰화 규칙을 설명적으로 지정하는 형식으로, 수많은 편집기에서 채택되었습니다. 하지만 한 가지 문제가 있었습니다. TextMate 문법은 수작업 토크나이저와 다르게 작동했습니다.
TextMate 문법은 `begin/end` 상태 또는 `while` 상태를 통해 여러 토큰에 걸쳐 있는 스코프를 푸시할 수 있습니다. 다음은 JavaScript TextMate 문법 아래에서의 동일한 예입니다(간결성을 위해 공백은 무시).

VS Code 1.8의 TextMate 문법
스코프 스택을 통과하면 각 토큰은 기본적으로 스코프 이름 배열을 받게 되며, 토크나이저에서 다음과 같은 결과를 얻게 됩니다.
tokens = [
{ startIndex: 0, scopes: ['source.js', 'meta.function.js', 'storage.type.function.js'] },
{ startIndex: 8, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 9,
scopes: [
'source.js',
'meta.function.js',
'meta.definition.function.js',
'entity.name.function.js'
]
},
{
startIndex: 11,
scopes: [
'source.js',
'meta.function.js',
'meta.parameters.js',
'punctuation.definition.parameters.js'
]
},
{ startIndex: 13, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 14,
scopes: [
'source.js',
'meta.function.js',
'meta.block.js',
'punctuation.definition.block.js'
]
}
];
모든 토큰 유형이 문자열이었고, 저희 코드는 문자열 배열을 처리할 준비가 되어 있지 않았습니다. 더구나 토큰의 이진 인코딩에 미치는 영향을 고려하면 더욱 그랬습니다. 따라서 다음과 같은 전략을 사용하여 스코프 배열을 단일 문자열로 "근사화"했습니다.
- 가장 덜 구체적인 스코프(예: `source.js`)를 무시했습니다. 거의 가치가 없었습니다.
- 나머지 각 스코프를 `.`으로 분할했습니다.
- 고유한 조각을 중복 제거했습니다.
- 나머지 조각을 안정적인 정렬 함수(반드시 사전순 정렬은 아님)로 정렬했습니다.
- 조각들을 `.`으로 결합했습니다.
tokens = [
{ startIndex: 0, type: 'meta.function.js.storage.type' },
{ startIndex: 9, type: 'meta.function.js' },
{ startIndex: 9, type: 'meta.function.js.definition.entity.name' },
{ startIndex: 11, type: 'meta.function.js.definition.parameters.punctuation' },
{ startIndex: 13, type: 'meta.function.js' },
{ startIndex: 14, type: 'meta.function.js.definition.punctuation.block' }
];
*: 저희가 했던 일은 순전히 잘못되었으며 "근사화"는 그것을 위한 아주 좋은 단어입니다. :)
이 토큰들은 "적합"하게 되고 수작업으로 작성된 토크나이저와 동일한 코드 경로(이진 인코딩)를 따르게 되며, 렌더링도 동일하게 이루어집니다.
<span class="token meta function js storage type">function</span>
<span class="token meta function js"> </span>
<span class="token meta function js definition entity name">f1</span>
<span class="token meta function js definition parameters punctuation">()</span>
<span class="token meta function js"> </span>
<span class="token meta function js definition punctuation block">{</span>
TextMate 테마
TextMate 테마는 특정 스코프를 가진 토큰을 선택하고 색상, 굵기 등 테마 정보를 적용하는 스코프 선택자와 함께 작동합니다.
다음과 같은 스코프를 가진 토큰이 주어졌을 때
// C B A
scopes = ['source.js', 'meta.definition.function.js', 'entity.name.function.js'];
다음은 일치하는 몇 가지 간단한 선택자로, 순위별로 정렬되었습니다(내림차순).
| 선택자 | C | B | A |
|---|---|---|---|
| source | source.js | meta.definition.function.js | entity.name.function.js |
| source.js | source.js | meta.definition.function.js | entity.name.function.js |
| meta | source.js | meta.definition.function.js | entity.name.function.js |
| meta.definition | source.js | meta.definition.function.js | entity.name.function.js |
| meta.definition.function | source.js | meta.definition.function.js | entity.name.function.js |
| entity | source.js | meta.definition.function.js | entity.name.function.js |
| entity.name | source.js | meta.definition.function.js | entity.name.function.js |
| entity.name.function | source.js | meta.definition.function.js | entity.name.function.js |
| entity.name.function.js | source.js | meta.definition.function.js | entity.name.function.js |
관찰: `entity`는 `meta.definition.function`보다 우선합니다. 왜냐하면 더 구체적인 스코프(`A` 대 `B`)와 일치하기 때문입니다.
관찰: `entity.name`은 `entity`보다 우선합니다. 왜냐하면 둘 다 동일한 스코프(`A`)와 일치하지만, `entity.name`이 `entity`보다 더 구체적이기 때문입니다.
부모 선택자
더 복잡하게 만들기 위해, TextMate 테마는 부모 선택자도 지원합니다. 다음은 간단한 선택자와 부모 선택자를 모두 사용하는 예입니다(순위 내림차순으로 다시 정렬).
| 선택자 | C | B | A |
|---|---|---|---|
| meta | source.js | meta.definition.function.js | entity.name.function.js |
| source meta | source.js | meta.definition.function.js | entity.name.function.js |
| source.js meta | source.js | meta.definition.function.js | entity.name.function.js |
| meta.definition | source.js | meta.definition.function.js | entity.name.function.js |
| source meta.definition | source.js | meta.definition.function.js | entity.name.function.js |
| entity | source.js | meta.definition.function.js | entity.name.function.js |
| source entity | source.js | meta.definition.function.js | entity.name.function.js |
| meta.definition entity | source.js | meta.definition.function.js | entity.name.function.js |
| entity.name | source.js | meta.definition.function.js | entity.name.function.js |
| source entity.name | source.js | meta.definition.function.js | entity.name.function.js |
관찰: `source entity`는 `entity`보다 우선합니다. 왜냐하면 둘 다 동일한 스코프(`A`)와 일치하지만, `source entity`는 부모 스코프(`C`)도 일치하기 때문입니다.
관찰: `entity.name`은 `source entity`보다 우선합니다. 왜냐하면 둘 다 동일한 스코프(`A`)와 일치하지만, `entity.name`이 `entity`보다 더 구체적이기 때문입니다.
참고: 세 번째 종류의 선택자가 있습니다. 스코프를 제외하는 것인데, 여기서는 논의하지 않겠습니다. 저희는 이것을 지원하지 않았으며, 실제로는 거의 사용되지 않는다는 것을 알게 되었습니다.
VS Code 1.8의 TextMate 테마
다음은 두 개의 Monokai 테마 규칙입니다(간결성을 위해 JSON 형식; 원본은 XML).
...
// Function name
{ "scope": "entity.name.function", "fontStyle": "", "foreground":"#A6E22E" }
...
// Class name
{ "scope": "entity.name.class", "fontStyle": "underline", "foreground":"#A6E22E" }
...
VS Code 1.8에서는 "근사화된" 스코프와 일치시키기 위해 다음과 같은 동적 CSS 규칙을 생성했습니다.
...
/* Function name */
.entity.name.function { color: #A6E22E; }
...
/* Class name */
.entity.name.class { color: #A6E22E; text-decoration: underline; }
...
그런 다음 CSS가 "근사화된" 스코프와 "근사화된" 규칙을 일치시키도록 맡겼습니다. 하지만 CSS 일치 규칙은 TextMate 선택자 일치 규칙과 다르며, 특히 순위 지정에 있어서는 더욱 그렇습니다. CSS 순위는 일치하는 클래스 이름의 수에 기반하는 반면, TextMate 선택자 순위는 스코프의 구체성에 대한 명확한 규칙을 가지고 있습니다.
그래서 VS Code의 TextMate 테마는 괜찮아 보였지만, 작성자의 의도대로 보인 적은 없었습니다. 때로는 차이가 미미했지만, 때로는 이러한 차이가 테마의 느낌을 완전히 바꿀 수 있었습니다.
몇 가지 별들의 정렬
시간이 지남에 따라 수작업 토크나이저를 점차 폐지했습니다(마지막인 HTML용은 불과 몇 달 전). 그래서 현재 VS Code에서는 모든 파일이 TextMate 문법으로 토큰화됩니다. Monaco Editor의 경우, 대부분의 지원 언어에 대해 TextMate 문법과 근본적으로 유사하지만 약간 더 표현력이 있고 브라우저에서 실행될 수 있는 설명적 토큰화 엔진인 Monarch로 마이그레이션했으며, 수동 토크나이저를 위한 래퍼를 추가했습니다. 전반적으로, 새로운 토큰화 형식을 지원하려면 3개의 토큰 제공자(TextMate, Monarch 및 수동 래퍼)를 변경해야 하며 10개 이상은 필요하지 않습니다.
몇 달 전에 VS Code 코어에서 토큰 유형을 읽는 모든 코드를 검토했으며, 해당 소비자들은 문자열, 정규식 또는 주석에만 관심이 있다는 것을 알게 되었습니다. 예를 들어, 괄호 일치 로직은 `"string"`, `"comment"`, 또는 `"regex"` 스코프를 포함하는 토큰을 무시합니다.
최근 내부 파트너(Monaco Editor를 사용하는 Microsoft 내의 다른 팀)로부터 Monaco Editor에서 IE9 및 IE10을 더 이상 지원하지 않아도 된다는 승인을 받았습니다.
아마도 가장 중요한 것은 편집기에서 가장 많은 표를 받은 기능이 minimap 지원이라는 것입니다. 합리적인 시간 내에 minimap을 렌더링하기 위해 DOM 노드와 CSS 일치를 사용할 수 없습니다. 아마도 캔버스를 사용할 것이며, JavaScript에서 각 토큰의 색상을 알아야 하므로 작은 글자를 올바른 색상으로 칠할 수 있어야 합니다.
아마도 가장 큰 진전은 토큰이나 그 스코프를 저장할 필요가 없다는 것입니다. 왜냐하면 토큰은 테마가 일치하거나 괄호 일치가 문자열을 건너뛰는 경우에만 영향을 주기 때문입니다.
마지막으로, VS Code 1.9의 새로운 기능
TextMate 테마 표현
아주 간단한 테마는 다음과 같을 수 있습니다.
theme = [
{ "foreground": "#F8F8F2" },
{ "scope": "var", "foreground": "#F8F8F2" },
{ "scope": "var.identifier", "foreground": "#00FF00", "fontStyle": "bold" },
{ "scope": "meta var.identifier", "foreground": "#0000FF" },
{ "scope": "constant", "foreground": "#100000", "fontStyle": "italic" },
{ "scope": "constant.numeric", "foreground": "#200000" },
{ "scope": "constant.numeric.hex", "fontStyle": "bold" },
{ "scope": "constant.numeric.oct", "fontStyle": "underline" },
{ "scope": "constant.numeric.dec", "foreground": "#300000" },
];
로딩 시, 테마에 나타나는 각 고유 색상에 ID를 생성하고 색상 맵에 저장합니다(이전에 토큰 유형에 대해 했던 것과 유사).
// 1 2 3 4 5 6
colorMap = ["reserved", "#F8F8F2", "#00FF00", "#0000FF", "#100000", "#200000", "#300000"]
theme = [
{ "foreground": 1 },
{ "scope": "var", "foreground": 1, },
{ "scope": "var.identifier", "foreground": 2, "fontStyle": "bold" },
{ "scope": "meta var.identifier", "foreground": 3 },
{ "scope": "constant", "foreground": 4, "fontStyle": "italic" },
{ "scope": "constant.numeric", "foreground": 5 },
{ "scope": "constant.numeric.hex", "fontStyle": "bold" },
{ "scope": "constant.numeric.oct", "fontStyle": "underline" },
{ "scope": "constant.numeric.dec", "foreground": 6 },
];
테마 규칙에서 트라이(Trie) 데이터 구조를 생성하며, 각 노드는 해결된 테마 옵션을 보유합니다.
관찰: `constant.numeric.hex` 및 `constant.numeric.oct` 노드는 `constant.numeric`에서 이 지침을 *상속*하기 때문에 전경색을 `5`로 변경하는 지침을 포함합니다.
관찰: `var.identifier` 노드는 추가 부모 규칙 `meta var.identifier`를 보유하며 해당 쿼리에 응답합니다.
스코프를 어떻게 테마화해야 하는지 알아내려면 이 트라이를 쿼리할 수 있습니다.
예를 들어,
| 쿼리 | 결과 |
|---|---|
| constant | 전경색을 4로, fontStyle을 italic으로 설정 |
| constant.numeric | 전경색을 5로, fontStyle을 italic으로 설정 |
| constant.numeric.hex | 전경색을 5로, fontStyle을 bold으로 설정 |
| var | 전경색을 1로 설정 |
| var.baz | 전경색을 1로 설정(var 일치) |
| baz | 아무것도 하지 않음(일치 없음) |
| var.identifier | 부모 스코프 meta가 있으면, 전경색을 3으로, fontStyle을 bold으로 설정, 그렇지 않으면, 전경색을 2로, fontStyle을 bold으로 설정 |
토큰화 변경 사항
VS Code에서 사용되는 모든 TextMate 토큰화 코드는 별도의 프로젝트인 vscode-textmate에 있으며, VS Code와 독립적으로 사용할 수 있습니다. `vscode-textmate`에서 스코프 스택을 표현하는 방식을 불변 연결 리스트로 변경했으며, 완전한 해결된 `metadata`도 저장합니다.
스코프 스택에 새 스코프를 푸시할 때, 테마 트라이에서 새 스코프를 조회합니다. 스코프 스택에서 상속받는 것과 테마 트라이가 반환하는 것을 기반으로 원하는 전경색 또는 글꼴 스타일을 즉시 계산할 수 있습니다.
일부 예제
| 스코프 스택 | 메타데이터 |
|---|---|
| ["source.js"] | 전경색은 1이고, 글꼴 스타일은 보통입니다(스코프 선택자가 없는 기본 규칙). |
| ["source.js","constant"] | 전경색은 4이고, fontStyle은 italic입니다. |
| ["source.js","constant","baz"] | 전경색은 4이고, fontStyle은 italic입니다. |
| ["source.js","var.identifier"] | 전경색은 2이고, fontStyle은 bold입니다. |
| ["source.js","meta","var.identifier"] | 전경색은 3이고, fontStyle은 bold입니다. |
스코프 스택에서 팝할 때, 이전 스코프 목록 요소와 함께 저장된 메타데이터를 사용할 수 있으므로 계산할 필요가 없습니다.
스코프 목록의 요소를 나타내는 TypeScript 클래스는 다음과 같습니다.
export class ScopeListElement {
public readonly parent: ScopeListElement;
public readonly scope: string;
public readonly metadata: number;
...
}
32비트의 메타데이터를 저장합니다.
/**
* - -------------------------------------------
* 3322 2222 2222 1111 1111 1100 0000 0000
* 1098 7654 3210 9876 5432 1098 7654 3210
* - -------------------------------------------
* xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
* bbbb bbbb bfff ffff ffFF FTTT LLLL LLLL
* - -------------------------------------------
* - L = LanguageId (8 bits)
* - T = StandardTokenType (3 bits)
* - F = FontStyle (3 bits)
* - f = foreground color (9 bits)
* - b = background color (9 bits)
*/
마지막으로, 토큰화 엔진에서 객체로 토큰을 내보내는 대신
// These are generated using the Monokai theme.
tokens_before = [
{ startIndex: 0, scopes: ['source.js', 'meta.function.js', 'storage.type.function.js'] },
{ startIndex: 8, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 9,
scopes: [
'source.js',
'meta.function.js',
'meta.definition.function.js',
'entity.name.function.js'
]
},
{
startIndex: 11,
scopes: [
'source.js',
'meta.function.js',
'meta.parameters.js',
'punctuation.definition.parameters.js'
]
},
{ startIndex: 13, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 14,
scopes: [
'source.js',
'meta.function.js',
'meta.block.js',
'punctuation.definition.block.js'
]
}
];
// Every even index is the token start index, every odd index is the token metadata.
// We get fewer tokens because tokens with the same metadata get collapsed
tokens_now = [
// bbbbbbbbb fffffffff FFF TTT LLLLLLLL
0,
16926743, // 000000010 000001001 001 000 00010111
8,
16793623, // 000000010 000000001 000 000 00010111
9,
16859159, // 000000010 000000101 000 000 00010111
11,
16793623 // 000000010 000000001 000 000 00010111
];
그리고 렌더링은 다음과 같이 이루어집니다.
<span class="mtk9 mtki">function</span>
<span class="mtk1"> </span>
<span class="mtk5">f1</span>
<span class="mtk1">() {</span>

토큰은 토크나이저에서 직접 Uint32Array로 반환됩니다. 백킹 ArrayBuffer를 유지하며, 위 예시의 경우 Chrome에서 96바이트가 소요됩니다. 요소 자체는 32바이트(8 x 32비트 숫자)만 소요되어야 하지만, 다시 v8 메타데이터 오버헤드가 관찰될 수 있습니다.
몇 가지 숫자
다음 측정값을 얻기 위해 서로 다른 특성과 다른 문법을 가진 세 개의 파일을 선택했습니다.
| 파일 이름 | 파일 크기 | 줄 수 | 언어 | 관찰 |
|---|---|---|---|---|
| checker.ts | 1.18 MB | 22,253 | TypeScript | TypeScript 컴파일러에서 사용된 실제 소스 파일 |
| bootstrap.min.css | 118.36 KB | 12 | CSS | 축소된 CSS 파일 |
| sqlite3.c | 6.73 MB | 200,904 | C | SQLite의 연결된 배포 파일 |
Windows의 다소 강력한 데스크톱 머신(Electron 32비트 사용)에서 테스트를 실행했습니다.
비교를 위해 소스 코드를 일부 수정해야 했습니다. 예를 들어, 두 VS Code 버전에서 정확히 동일한 문법을 사용하고, 두 버전 모두에서 풍부한 언어 기능을 비활성화하고, VS Code 1.8의 100 스택 깊이 제한을 제거했습니다(VS Code 1.9에서는 더 이상 존재하지 않음). 또한 bootstrap.min.css를 여러 줄로 분할하여 각 줄을 20k 문자 미만으로 만들어야 했습니다.
토큰화 시간
토큰화는 UI 스레드에서 양보 방식으로 실행되므로, 다음 시간을 측정하기 위해 동기식으로 실행하도록 강제하는 코드를 추가해야 했습니다(10회 실행의 중앙값 표시).
| 파일 이름 | 파일 크기 | VS Code 1.8 | VS Code 1.9 | 속도 향상 |
|---|---|---|---|---|
| checker.ts | 1.18 MB | 4606.80 ms | 3939.00 ms | 14.50% |
| bootstrap.min.css | 118.36 KB | 776.76 ms | 416.28 ms | 46.41% |
| sqlite3.c | 6.73 MB | 16010.42 ms | 10964.42 ms | 31.52% |
토큰화는 이제 테마 일치도 수행하지만, 시간 절약은 각 줄을 한 번만 통과하면 되기 때문입니다. 이전에는 토큰화 패스, 스코프를 문자열로 "근사화"하는 두 번째 패스, 토큰을 이진 인코딩하는 세 번째 패스가 있었지만, 이제 토큰은 TextMate 토큰화 엔진에서 직접 이진 인코딩 방식으로 생성됩니다. 가비지 컬렉션이 필요한 생성 객체의 수도 상당히 줄었습니다.
메모리 사용량
특히 대형 파일의 경우 폴딩이 많은 메모리를 소비합니다(이것은 나중에 최적화할 사항). 따라서 폴딩이 꺼진 상태에서 수집한 힙 스냅샷 수치입니다. 이는 원본 파일 문자열을 제외한 모델이 보유한 메모리를 보여줍니다.
| 파일 이름 | 파일 크기 | VS Code 1.8 | VS Code 1.9 | 메모리 절감 |
|---|---|---|---|---|
| checker.ts | 1.18 MB | 3.37 MB | 2.61 MB | 22.60% |
| bootstrap.min.css | 118.36 KB | 267.00 KB | 201.33 KB | 24.60% |
| sqlite3.c | 6.73 MB | 27.49 MB | 21.22 MB | 22.83% |
메모리 사용량 감소는 더 이상 토큰 맵을 유지하지 않고, 동일한 메타데이터를 가진 연속적인 토큰을 압축하며, `ArrayBuffer`를 백킹 스토어로 사용하기 때문입니다. 공백만 있는 토큰을 이전 토큰으로 항상 압축하면 더 개선될 수 있습니다. 공백의 색상은 중요하지 않기 때문입니다(공백은 보이지 않습니다).
새로운 TextMate 스코프 검사기 위젯
테마 또는 문법 작성 및 디버깅을 돕기 위해 새로운 위젯을 추가했습니다. **명령 팔레트**에서 **Developer: Inspect Editor Tokens and Scopes**를 실행하여 실행할 수 있습니다. (OSX: ⇧⌘P, Windows/Linux: Ctrl+Shift+P).

변경 사항 검증
편집기의 이 구성 요소에 변경 사항을 적용하는 것은 심각한 위험을 수반했습니다. 새로운 트라이 생성 코드, 새로운 이진 인코딩 형식 등 저희 접근 방식의 모든 버그는 사용자에게 큰 차이를 초래할 수 있기 때문입니다.
VS Code에는 우리가 제공하는 모든 프로그래밍 언어에 대해 우리가 작성한 다섯 가지 테마(Light, Light+, Dark, Dark+, High Contrast)에 대한 색상을 검증하는 통합 제품군이 있습니다. 이러한 테스트는 테마 중 하나를 수정하거나 특정 문법을 업데이트할 때 모두 매우 유용합니다. 73개의 통합 테스트 각각은 고정 파일(예: test.c)과 다섯 가지 테마에 대한 예상 색상(test_c.json)으로 구성되며, CI 빌드에서 실행됩니다.
토큰화 변경을 검증하기 위해, 이전 CSS 기반 접근 방식을 사용하여 우리가 제공하는 14개 테마(우리가 작성한 5개 테마뿐만 아니라) 모두에 대한 이러한 테스트의 색상 결과를 수집했습니다. 그런 다음, 각 변경 후에 새로운 트라이 기반 로직을 사용하여 동일한 테스트를 실행했고, 사용자 정의 시각적 차이(및 패치) 도구를 사용하여 각 색상 차이를 조사하고 색상 변경의 근본 원인을 파악했습니다. 이 기법을 통해 최소 2개의 버그를 발견했으며, VS Code 버전 간의 색상 변경을 최소화하기 위해 5가지 테마를 변경할 수 있었습니다.

이전과 이후
아래는 VS Code 1.8과 현재 VS Code 1.9에서 표시된 다양한 색상 테마입니다.
Monokai 테마


Quiet Light 테마


Red 테마


결론
VS Code 1.9로 업그레이드하여 추가 CPU 시간과 RAM을 활용할 수 있기를 바라며, 앞으로도 여러분이 효율적이고 즐겁게 코딩할 수 있도록 계속 지원하겠습니다.
즐거운 코딩 되세요!
Alexandru Dima, VS Code 팀원 @alexdima123