From 0899758daeebc6d5429679b4e9b5d76ee9794273 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 2 Dec 2022 15:54:36 -0800 Subject: [PATCH] ipc: use vql for uint types (#167407) * ipc: use vql for uint types On the plane I was reverse-engineering ipc.ts to implement it in Rust and see if we could have a "service mode" for the CLI that we could interact with like any other vscode process. In doing so, I noticed that numbers in the protocol--which are used at least twice in the message header and ID--were encoded as JSON. I was curious what benefits we'd get from encoding them as variable-length integers instead. It makes the message shorter, as expected. Encode/decode time are very, very slightly lower. I'm not sure it's worth the extra complexity, but I have included it here for your consideration. * fixup tests --- src/vs/base/parts/ipc/common/ipc.ts | 90 +++++++++++++------ src/vs/base/parts/ipc/test/common/ipc.test.ts | 18 +++- test/unit/README.md | 2 +- 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 90041425e03..1d8c4a45efa 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -164,7 +164,50 @@ interface IWriter { write(buffer: VSBuffer): void; } -class BufferReader implements IReader { + +/** + * @see https://en.wikipedia.org/wiki/Variable-length_quantity + */ +function readIntVQL(reader: IReader) { + let value = 0; + for (let n = 0; ; n += 7) { + const next = reader.read(1); + value |= (next.buffer[0] & 0b01111111) << n; + if (!(next.buffer[0] & 0b10000000)) { + return value; + } + } +} + +const vqlZero = createOneByteBuffer(0); + +/** + * @see https://en.wikipedia.org/wiki/Variable-length_quantity + */ +function writeInt32VQL(writer: IWriter, value: number) { + if (value === 0) { + writer.write(vqlZero); + return; + } + + let len = 0; + for (let v2 = value; v2 !== 0; v2 = v2 >>> 7) { + len++; + } + + const scratch = VSBuffer.alloc(len); + for (let i = 0; value !== 0; i++) { + scratch.buffer[i] = value & 0b01111111; + value = value >>> 7; + if (value > 0) { + scratch.buffer[i] |= 0b10000000; + } + } + + writer.write(scratch); +} + +export class BufferReader implements IReader { private pos = 0; @@ -177,7 +220,7 @@ class BufferReader implements IReader { } } -class BufferWriter implements IWriter { +export class BufferWriter implements IWriter { private buffers: VSBuffer[] = []; @@ -196,17 +239,8 @@ enum DataType { Buffer = 2, VSBuffer = 3, Array = 4, - Object = 5 -} - -function createSizeBuffer(size: number): VSBuffer { - const result = VSBuffer.alloc(4); - result.writeUInt32BE(size, 0); - return result; -} - -function readSizeBuffer(reader: IReader): number { - return reader.read(4).readUInt32BE(0); + Object = 5, + Int = 6 } function createOneByteBuffer(value: number): VSBuffer { @@ -222,53 +256,58 @@ const BufferPresets = { VSBuffer: createOneByteBuffer(DataType.VSBuffer), Array: createOneByteBuffer(DataType.Array), Object: createOneByteBuffer(DataType.Object), + Uint: createOneByteBuffer(DataType.Int), }; declare const Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); -function serialize(writer: IWriter, data: any): void { +export function serialize(writer: IWriter, data: any): void { if (typeof data === 'undefined') { writer.write(BufferPresets.Undefined); } else if (typeof data === 'string') { const buffer = VSBuffer.fromString(data); writer.write(BufferPresets.String); - writer.write(createSizeBuffer(buffer.byteLength)); + writeInt32VQL(writer, buffer.byteLength); writer.write(buffer); } else if (hasBuffer && Buffer.isBuffer(data)) { const buffer = VSBuffer.wrap(data); writer.write(BufferPresets.Buffer); - writer.write(createSizeBuffer(buffer.byteLength)); + writeInt32VQL(writer, buffer.byteLength); writer.write(buffer); } else if (data instanceof VSBuffer) { writer.write(BufferPresets.VSBuffer); - writer.write(createSizeBuffer(data.byteLength)); + writeInt32VQL(writer, data.byteLength); writer.write(data); } else if (Array.isArray(data)) { writer.write(BufferPresets.Array); - writer.write(createSizeBuffer(data.length)); + writeInt32VQL(writer, data.length); for (const el of data) { serialize(writer, el); } + } else if (typeof data === 'number' && (data | 0) === data) { + // write a vql if it's a number that we can do bitwise operations on + writer.write(BufferPresets.Uint); + writeInt32VQL(writer, data); } else { const buffer = VSBuffer.fromString(JSON.stringify(data)); writer.write(BufferPresets.Object); - writer.write(createSizeBuffer(buffer.byteLength)); + writeInt32VQL(writer, buffer.byteLength); writer.write(buffer); } } -function deserialize(reader: IReader): any { +export function deserialize(reader: IReader): any { const type = reader.read(1).readUInt8(0); switch (type) { case DataType.Undefined: return undefined; - case DataType.String: return reader.read(readSizeBuffer(reader)).toString(); - case DataType.Buffer: return reader.read(readSizeBuffer(reader)).buffer; - case DataType.VSBuffer: return reader.read(readSizeBuffer(reader)); + case DataType.String: return reader.read(readIntVQL(reader)).toString(); + case DataType.Buffer: return reader.read(readIntVQL(reader)).buffer; + case DataType.VSBuffer: return reader.read(readIntVQL(reader)); case DataType.Array: { - const length = readSizeBuffer(reader); + const length = readIntVQL(reader); const result: any[] = []; for (let i = 0; i < length; i++) { @@ -277,7 +316,8 @@ function deserialize(reader: IReader): any { return result; } - case DataType.Object: return JSON.parse(reader.read(readSizeBuffer(reader)).toString()); + case DataType.Object: return JSON.parse(reader.read(readIntVQL(reader)).toString()); + case DataType.Int: return readIntVQL(reader); } } diff --git a/src/vs/base/parts/ipc/test/common/ipc.test.ts b/src/vs/base/parts/ipc/test/common/ipc.test.ts index d4a9a4bbb08..eaead87178e 100644 --- a/src/vs/base/parts/ipc/test/common/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/common/ipc.test.ts @@ -11,7 +11,7 @@ import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ClientConnectionEvent, IChannel, IMessagePassingProtocol, IPCClient, IPCServer, IServerChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; +import { BufferReader, BufferWriter, ClientConnectionEvent, deserialize, IChannel, IMessagePassingProtocol, IPCClient, IPCServer, IServerChannel, ProxyChannel, serialize } from 'vs/base/parts/ipc/common/ipc'; class QueueProtocol implements IMessagePassingProtocol { @@ -319,6 +319,22 @@ suite('Base IPC', function () { const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]); return assert.strictEqual(r, 5); }); + + test('round trips numbers', () => { + const input = [ + 0, + 1, + -1, + 12345, + -12345, + 42.6, + 123412341234 + ]; + + const writer = new BufferWriter(); + serialize(writer, input); + assert.deepStrictEqual(deserialize(new BufferReader(writer.buffer)), input); + }); }); suite('one to one (proxy)', function () { diff --git a/test/unit/README.md b/test/unit/README.md index 8ac82b99d8c..9ad1c7a6a15 100644 --- a/test/unit/README.md +++ b/test/unit/README.md @@ -27,7 +27,7 @@ Unit tests from layers `common` and `browser` are run inside `chromium`, `webkit ## Run (with node) - yarn run mocha --ui tdd --run src/vs/editor/test/browser/controller/cursor.test.ts + yarn test-node --run src/vs/editor/test/browser/controller/cursor.test.ts ## Coverage