Skip to content

Commit a297639

Browse files
committed
Store closed edit stack elements as ArrayBuffer to reduce memory usage
1 parent e9d6a9a commit a297639

5 files changed

Lines changed: 251 additions & 32 deletions

File tree

src/vs/base/common/buffer.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ export class VSBuffer {
115115
}
116116
}
117117

118+
export function readUInt16LE(source: Uint8Array, offset: number): number {
119+
return (
120+
source[offset]
121+
+ source[offset + 1] * 2 ** 8
122+
);
123+
}
124+
125+
export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {
126+
destination[offset] = value;
127+
value = value >>> 8;
128+
destination[offset + 1] = value;
129+
}
130+
118131
export function readUInt32BE(source: Uint8Array, offset: number): number {
119132
return (
120133
source[offset] * 2 ** 24
@@ -134,11 +147,11 @@ export function writeUInt32BE(destination: Uint8Array, value: number, offset: nu
134147
destination[offset] = value;
135148
}
136149

137-
function readUInt8(source: Uint8Array, offset: number): number {
150+
export function readUInt8(source: Uint8Array, offset: number): number {
138151
return source[offset];
139152
}
140153

141-
function writeUInt8(destination: Uint8Array, value: number, offset: number): void {
154+
export function writeUInt8(destination: Uint8Array, value: number, offset: number): void {
142155
destination[offset] = value;
143156
}
144157

src/vs/base/common/platform.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,17 @@ export const enum OperatingSystem {
209209
Linux = 3
210210
}
211211
export const OS = (_isMacintosh ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
212+
213+
let _isLittleEndian = true;
214+
let _isLittleEndianComputed = false;
215+
export function isLittleEndian(): boolean {
216+
if (!_isLittleEndianComputed) {
217+
_isLittleEndianComputed = true;
218+
const test = new Uint8Array(2);
219+
test[0] = 1;
220+
test[1] = 2;
221+
const view = new Uint16Array(test.buffer);
222+
_isLittleEndian = (view[0] === (2 << 8) + 1);
223+
}
224+
return _isLittleEndian;
225+
}

src/vs/editor/common/core/stringBuilder.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as strings from 'vs/base/common/strings';
7+
import * as platform from 'vs/base/common/platform';
8+
import * as buffer from 'vs/base/common/buffer';
79

8-
declare const TextDecoder: any; // TODO@TypeScript
10+
declare const TextDecoder: {
11+
prototype: TextDecoder;
12+
new(label?: string, options?: TextDecoderOptions): TextDecoder;
13+
};
914
interface TextDecoder {
1015
decode(view: Uint16Array): string;
1116
}
@@ -18,25 +23,49 @@ export interface IStringBuilder {
1823
appendASCIIString(str: string): void;
1924
}
2025

26+
let _platformTextDecoder: TextDecoder | null;
27+
function getPlatformTextDecoder(): TextDecoder {
28+
if (!_platformTextDecoder) {
29+
_platformTextDecoder = new TextDecoder(platform.isLittleEndian() ? 'UTF-16LE' : 'UTF-16BE');
30+
}
31+
return _platformTextDecoder;
32+
}
33+
2134
export let createStringBuilder: (capacity: number) => IStringBuilder;
35+
export let decodeUTF16LE: (source: Uint8Array, offset: number, len: number) => string;
2236

2337
if (typeof TextDecoder !== 'undefined') {
2438
createStringBuilder = (capacity) => new StringBuilder(capacity);
39+
decodeUTF16LE = standardDecodeUTF16LE;
2540
} else {
2641
createStringBuilder = (capacity) => new CompatStringBuilder();
42+
decodeUTF16LE = compatDecodeUTF16LE;
43+
}
44+
45+
function standardDecodeUTF16LE(source: Uint8Array, offset: number, len: number): string {
46+
const view = new Uint16Array(source.buffer, offset, len);
47+
return getPlatformTextDecoder().decode(view);
48+
}
49+
50+
function compatDecodeUTF16LE(source: Uint8Array, offset: number, len: number): string {
51+
let result: string[] = [];
52+
let resultLen = 0;
53+
for (let i = 0; i < len; i++) {
54+
const charCode = buffer.readUInt16LE(source, offset); offset += 2;
55+
result[resultLen++] = String.fromCharCode(charCode);
56+
}
57+
return result.join('');
2758
}
2859

2960
class StringBuilder implements IStringBuilder {
3061

31-
private readonly _decoder: TextDecoder;
3262
private readonly _capacity: number;
3363
private readonly _buffer: Uint16Array;
3464

3565
private _completedStrings: string[] | null;
3666
private _bufferLength: number;
3767

3868
constructor(capacity: number) {
39-
this._decoder = new TextDecoder('UTF-16LE');
4069
this._capacity = capacity | 0;
4170
this._buffer = new Uint16Array(this._capacity);
4271

@@ -63,7 +92,7 @@ class StringBuilder implements IStringBuilder {
6392
}
6493

6594
const view = new Uint16Array(this._buffer.buffer, 0, this._bufferLength);
66-
return this._decoder.decode(view);
95+
return getPlatformTextDecoder().decode(view);
6796
}
6897

6998
private _flushBuffer(): void {

src/vs/editor/common/model/editStack.ts

Lines changed: 140 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,133 @@ import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorks
1212
import { URI } from 'vs/base/common/uri';
1313
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
1414
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
15+
import * as buffer from 'vs/base/common/buffer';
16+
17+
class SingleModelEditStackData {
18+
19+
public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData {
20+
const alternativeVersionId = model.getAlternativeVersionId();
21+
const eol = getModelEOL(model);
22+
return new SingleModelEditStackData(
23+
alternativeVersionId,
24+
alternativeVersionId,
25+
eol,
26+
eol,
27+
beforeCursorState,
28+
beforeCursorState,
29+
[]
30+
);
31+
}
32+
33+
constructor(
34+
public readonly beforeVersionId: number,
35+
public afterVersionId: number,
36+
public readonly beforeEOL: EndOfLineSequence,
37+
public afterEOL: EndOfLineSequence,
38+
public readonly beforeCursorState: Selection[] | null,
39+
public afterCursorState: Selection[] | null,
40+
public changes: TextChange[]
41+
) { }
42+
43+
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
44+
if (operations.length > 0) {
45+
this.changes = compressConsecutiveTextChanges(this.changes, operations.map(op => op.textChange));
46+
}
47+
this.afterEOL = afterEOL;
48+
this.afterVersionId = afterVersionId;
49+
this.afterCursorState = afterCursorState;
50+
}
51+
52+
private static _writeSelectionsSize(selections: Selection[] | null): number {
53+
return 4 + 4 * 4 * (selections ? selections.length : 0);
54+
}
55+
56+
private static _writeSelections(b: Uint8Array, selections: Selection[] | null, offset: number): number {
57+
buffer.writeUInt32BE(b, (selections ? selections.length : 0), offset); offset += 4;
58+
if (selections) {
59+
for (const selection of selections) {
60+
buffer.writeUInt32BE(b, selection.selectionStartLineNumber, offset); offset += 4;
61+
buffer.writeUInt32BE(b, selection.selectionStartColumn, offset); offset += 4;
62+
buffer.writeUInt32BE(b, selection.positionLineNumber, offset); offset += 4;
63+
buffer.writeUInt32BE(b, selection.positionColumn, offset); offset += 4;
64+
}
65+
}
66+
return offset;
67+
}
68+
69+
private static _readSelections(b: Uint8Array, offset: number, dest: Selection[]): number {
70+
const count = buffer.readUInt32BE(b, offset); offset += 4;
71+
for (let i = 0; i < count; i++) {
72+
const selectionStartLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
73+
const selectionStartColumn = buffer.readUInt32BE(b, offset); offset += 4;
74+
const positionLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
75+
const positionColumn = buffer.readUInt32BE(b, offset); offset += 4;
76+
dest.push(new Selection(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn));
77+
}
78+
return offset;
79+
}
80+
81+
public serialize(): ArrayBuffer {
82+
let necessarySize = (
83+
+ 4 // beforeVersionId
84+
+ 4 // afterVersionId
85+
+ 1 // beforeEOL
86+
+ 1 // afterEOL
87+
+ SingleModelEditStackData._writeSelectionsSize(this.beforeCursorState)
88+
+ SingleModelEditStackData._writeSelectionsSize(this.afterCursorState)
89+
+ 4 // change count
90+
);
91+
for (const change of this.changes) {
92+
necessarySize += change.writeSize();
93+
}
94+
95+
const b = new Uint8Array(necessarySize);
96+
let offset = 0;
97+
buffer.writeUInt32BE(b, this.beforeVersionId, offset); offset += 4;
98+
buffer.writeUInt32BE(b, this.afterVersionId, offset); offset += 4;
99+
buffer.writeUInt8(b, this.beforeEOL, offset); offset += 1;
100+
buffer.writeUInt8(b, this.afterEOL, offset); offset += 1;
101+
offset = SingleModelEditStackData._writeSelections(b, this.beforeCursorState, offset);
102+
offset = SingleModelEditStackData._writeSelections(b, this.afterCursorState, offset);
103+
buffer.writeUInt32BE(b, this.changes.length, offset); offset += 4;
104+
for (const change of this.changes) {
105+
offset = change.write(b, offset);
106+
}
107+
return b.buffer;
108+
}
109+
110+
public static deserialize(source: ArrayBuffer): SingleModelEditStackData {
111+
const b = new Uint8Array(source);
112+
let offset = 0;
113+
const beforeVersionId = buffer.readUInt32BE(b, offset); offset += 4;
114+
const afterVersionId = buffer.readUInt32BE(b, offset); offset += 4;
115+
const beforeEOL = buffer.readUInt8(b, offset); offset += 1;
116+
const afterEOL = buffer.readUInt8(b, offset); offset += 1;
117+
const beforeCursorState: Selection[] = [];
118+
offset = SingleModelEditStackData._readSelections(b, offset, beforeCursorState);
119+
const afterCursorState: Selection[] = [];
120+
offset = SingleModelEditStackData._readSelections(b, offset, afterCursorState);
121+
const changeCount = buffer.readUInt32BE(b, offset); offset += 4;
122+
const changes: TextChange[] = [];
123+
for (let i = 0; i < changeCount; i++) {
124+
offset = TextChange.read(b, offset, changes);
125+
}
126+
return new SingleModelEditStackData(
127+
beforeVersionId,
128+
afterVersionId,
129+
beforeEOL,
130+
afterEOL,
131+
beforeCursorState,
132+
afterCursorState,
133+
changes
134+
);
135+
}
136+
}
15137

16138
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
17139

18-
private _isOpen: boolean;
19140
public model: ITextModel;
20-
private readonly _beforeVersionId: number;
21-
private readonly _beforeEOL: EndOfLineSequence;
22-
private readonly _beforeCursorState: Selection[] | null;
23-
private _afterVersionId: number;
24-
private _afterEOL: EndOfLineSequence;
25-
private _afterCursorState: Selection[] | null;
26-
private _changes: TextChange[];
141+
private _data: SingleModelEditStackData | ArrayBuffer;
27142

28143
public get type(): UndoRedoElementType.Resource {
29144
return UndoRedoElementType.Resource;
@@ -38,45 +153,44 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
38153
}
39154

40155
constructor(model: ITextModel, beforeCursorState: Selection[] | null) {
41-
this._isOpen = true;
42156
this.model = model;
43-
this._beforeVersionId = this.model.getAlternativeVersionId();
44-
this._beforeEOL = getModelEOL(this.model);
45-
this._beforeCursorState = beforeCursorState;
46-
this._afterVersionId = this._beforeVersionId;
47-
this._afterEOL = this._beforeEOL;
48-
this._afterCursorState = this._beforeCursorState;
49-
this._changes = [];
157+
this._data = SingleModelEditStackData.create(model, beforeCursorState);
50158
}
51159

52160
public setModel(model: ITextModel): void {
53161
this.model = model;
54162
}
55163

56164
public canAppend(model: ITextModel): boolean {
57-
return (this._isOpen && this.model === model);
165+
return (this.model === model && this._data instanceof SingleModelEditStackData);
58166
}
59167

60168
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
61-
if (operations.length > 0) {
62-
this._changes = compressConsecutiveTextChanges(this._changes, operations.map(op => op.textChange));
169+
if (this._data instanceof SingleModelEditStackData) {
170+
this._data.append(model, operations, afterEOL, afterVersionId, afterCursorState);
63171
}
64-
this._afterEOL = afterEOL;
65-
this._afterVersionId = afterVersionId;
66-
this._afterCursorState = afterCursorState;
67172
}
68173

69174
public close(): void {
70-
this._isOpen = false;
175+
if (this._data instanceof SingleModelEditStackData) {
176+
this._data = this._data.serialize();
177+
}
71178
}
72179

73180
public undo(): void {
74-
this._isOpen = false;
75-
this.model._applyUndo(this._changes, this._beforeEOL, this._beforeVersionId, this._beforeCursorState);
181+
if (this._data instanceof SingleModelEditStackData) {
182+
this._data = this._data.serialize();
183+
}
184+
const data = SingleModelEditStackData.deserialize(this._data);
185+
this.model._applyUndo(data.changes, data.beforeEOL, data.beforeVersionId, data.beforeCursorState);
76186
}
77187

78188
public redo(): void {
79-
this.model._applyRedo(this._changes, this._afterEOL, this._afterVersionId, this._afterCursorState);
189+
if (this._data instanceof SingleModelEditStackData) {
190+
this._data = this._data.serialize();
191+
}
192+
const data = SingleModelEditStackData.deserialize(this._data);
193+
this.model._applyRedo(data.changes, data.afterEOL, data.afterVersionId, data.afterCursorState);
80194
}
81195
}
82196

src/vs/editor/common/model/textChange.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as buffer from 'vs/base/common/buffer';
7+
import { decodeUTF16LE } from 'vs/editor/common/core/stringBuilder';
8+
69
export class TextChange {
710

811
public get oldLength(): number {
@@ -27,6 +30,52 @@ export class TextChange {
2730
public readonly newPosition: number,
2831
public readonly newText: string
2932
) { }
33+
34+
private static _writeStringSize(str: string): number {
35+
return (
36+
4 + 2 * str.length
37+
);
38+
}
39+
40+
private static _writeString(b: Uint8Array, str: string, offset: number): number {
41+
const len = str.length;
42+
buffer.writeUInt32BE(b, len, offset); offset += 4;
43+
for (let i = 0; i < len; i++) {
44+
buffer.writeUInt16LE(b, str.charCodeAt(i), offset); offset += 2;
45+
}
46+
return offset;
47+
}
48+
49+
private static _readString(b: Uint8Array, offset: number): string {
50+
const len = buffer.readUInt32BE(b, offset); offset += 4;
51+
return decodeUTF16LE(b, offset, len);
52+
}
53+
54+
public writeSize(): number {
55+
return (
56+
+ 4 // oldPosition
57+
+ 4 // newPosition
58+
+ TextChange._writeStringSize(this.oldText)
59+
+ TextChange._writeStringSize(this.newText)
60+
);
61+
}
62+
63+
public write(b: Uint8Array, offset: number): number {
64+
buffer.writeUInt32BE(b, this.oldPosition, offset); offset += 4;
65+
buffer.writeUInt32BE(b, this.newPosition, offset); offset += 4;
66+
offset = TextChange._writeString(b, this.oldText, offset);
67+
offset = TextChange._writeString(b, this.newText, offset);
68+
return offset;
69+
}
70+
71+
public static read(b: Uint8Array, offset: number, dest: TextChange[]): number {
72+
const oldPosition = buffer.readUInt32BE(b, offset); offset += 4;
73+
const newPosition = buffer.readUInt32BE(b, offset); offset += 4;
74+
const oldText = TextChange._readString(b, offset); offset += TextChange._writeStringSize(oldText);
75+
const newText = TextChange._readString(b, offset); offset += TextChange._writeStringSize(newText);
76+
dest.push(new TextChange(oldPosition, oldText, newPosition, newText));
77+
return offset;
78+
}
3079
}
3180

3281
export function compressConsecutiveTextChanges(prevEdits: TextChange[] | null, currEdits: TextChange[]): TextChange[] {

0 commit comments

Comments
 (0)