From 18ff9e2cfc3db4a05c59e0609c7916b6e5fe96b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Tue, 26 Apr 2022 09:47:14 +0200 Subject: [PATCH] Performance optimisations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- src/array-buffer-message-buffer.spec.ts | 4 +- src/array-buffer-message-buffer.ts | 86 ++++++++++----- src/channel.ts | 4 +- src/experiments.ts | 136 +++++++++++++++++++----- src/message-buffer.ts | 21 +++- src/message-encoder.spec.ts | 2 +- src/message-encoder.ts | 56 ++++++++-- src/websocket-client-channel.ts | 2 +- 8 files changed, 243 insertions(+), 68 deletions(-) diff --git a/src/array-buffer-message-buffer.spec.ts b/src/array-buffer-message-buffer.spec.ts index dfcaa1d..7bbb8d6 100644 --- a/src/array-buffer-message-buffer.spec.ts +++ b/src/array-buffer-message-buffer.spec.ts @@ -18,7 +18,7 @@ import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-m describe('array message buffer tests', () => { it('basic read write test', () => { - const buffer = new ArrayBuffer(1024); + const buffer = new Uint8Array(1024); const writer = new ArrrayBufferWriteBuffer(buffer); writer.writeByte(8); @@ -34,7 +34,7 @@ describe('array message buffer tests', () => { expect(reader.readByte()).equal(8); expect(reader.readInt()).equal(10000) - expect(reader.readBytes()).deep.equal(new Uint8Array([1, 2, 3, 4]).buffer); + expect(reader.readBytes()).deep.equal(new Uint8Array([1, 2, 3, 4])); expect(reader.readString()).equal('this is a string'); expect(reader.readString()).equal('another string'); }) diff --git a/src/array-buffer-message-buffer.ts b/src/array-buffer-message-buffer.ts index 852fc16..8efb70a 100644 --- a/src/array-buffer-message-buffer.ts +++ b/src/array-buffer-message-buffer.ts @@ -17,11 +17,11 @@ import { Emitter, Event } from './env/event'; import { ReadBuffer, WriteBuffer } from './message-buffer'; export class ArrrayBufferWriteBuffer implements WriteBuffer { - constructor(private buffer: ArrayBuffer = new ArrayBuffer(1024), private offset: number = 0) { - } + private encoder = new TextEncoder(); + private msg: DataView; - private get msg() { - return new DataView(this.buffer); + constructor(private buffer: Uint8Array = new Uint8Array(1024 * 1024), private offset: number = 0) { + this.msg = new DataView(buffer.buffer); } ensureCapacity(value: number): WriteBuffer { @@ -30,19 +30,37 @@ export class ArrrayBufferWriteBuffer implements WriteBuffer { newLength *= 2; } if (newLength !== this.buffer.byteLength) { - const newBuffer = new ArrayBuffer(newLength); - new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)); + console.log("reallocating to " + newLength); + const newBuffer = new Uint8Array(newLength); + newBuffer.set(this.buffer); this.buffer = newBuffer; + this.msg = new DataView(this.buffer.buffer); + } + return this; + } + + writeLength(length: number): WriteBuffer { + if (length < 127) { + this.writeByte(length); + } else { + this.writeByte(128 + (length & 127)); + this.writeLength(length >> 7); } return this; } writeByte(value: number): WriteBuffer { this.ensureCapacity(1); - this.msg.setUint8(this.offset++, value); + this.buffer[this.offset++] = value; return this; } + writeNumber(value: number): WriteBuffer { + this.ensureCapacity(8); + this.msg.setFloat64(this.offset, value); + this.offset += 8; + return this; + } writeInt(value: number): WriteBuffer { this.ensureCapacity(4); @@ -52,19 +70,21 @@ export class ArrrayBufferWriteBuffer implements WriteBuffer { } writeString(value: string): WriteBuffer { - const encoded = this.encodeString(value); - this.writeBytes(encoded); + this.ensureCapacity(4 * value.length); + const result = this.encoder.encodeInto(value, this.buffer.subarray(this.offset + 4)); + this.msg.setUint32(this.offset, result.written!); + this.offset += 4 + result.written!; return this; } - private encodeString(value: string): Uint8Array { - return new TextEncoder().encode(value); + encodeString(value: string): Uint8Array { + return this.encoder.encode(value); } - writeBytes(value: ArrayBuffer): WriteBuffer { - this.ensureCapacity(value.byteLength + 4); - this.writeInt(value.byteLength); - new Uint8Array(this.buffer).set(new Uint8Array(value), this.offset); + writeBytes(value: Uint8Array): WriteBuffer { + this.writeLength(value.byteLength); + this.ensureCapacity(value.length); + this.buffer.set(value, this.offset); this.offset += value.byteLength; return this; } @@ -78,25 +98,41 @@ export class ArrrayBufferWriteBuffer implements WriteBuffer { this.onCommitEmitter.fire(this.getCurrentContents()); } - getCurrentContents(): ArrayBuffer { + getCurrentContents(): Uint8Array { return this.buffer.slice(0, this.offset); } } export class ArrayBufferReadBuffer implements ReadBuffer { private offset: number = 0; + private msg; - constructor(private readonly buffer: ArrayBuffer) { - } - - private get msg(): DataView { - return new DataView(this.buffer); + constructor(private readonly buffer: Uint8Array) { + this.msg = new DataView(buffer.buffer); } readByte(): number { return this.msg.getUint8(this.offset++); } + readLength(): number { + let shift = 0; + let byte = this.readByte(); + let value = (byte & 127) << shift; + while (byte > 127) { + shift += 7; + byte = this.readByte(); + value = value + ((byte & 127) << shift); + } + return value; + } + + readNumber(): number { + const result = this.msg.getFloat64(this.offset); + this.offset += 8; + return result; + } + readInt(): number { const result = this.msg.getInt32(this.offset); this.offset += 4; @@ -104,8 +140,7 @@ export class ArrayBufferReadBuffer implements ReadBuffer { } readString(): string { - const len = this.msg.getUint32(this.offset); - this.offset += 4; + const len = this.readInt(); const result = this.decodeString(this.buffer.slice(this.offset, this.offset + len)); this.offset += len; return result; @@ -115,9 +150,8 @@ export class ArrayBufferReadBuffer implements ReadBuffer { return new TextDecoder().decode(buf); } - readBytes(): ArrayBuffer { - const length = this.msg.getUint32(this.offset); - this.offset += 4; + readBytes(): Uint8Array { + const length = this.readLength(); const result = this.buffer.slice(this.offset, this.offset + length); this.offset += length; return result; diff --git a/src/channel.ts b/src/channel.ts index 32be53d..c5f6490 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -201,14 +201,14 @@ export class ChannelPipe { readonly left: ForwardingChannel = new ForwardingChannel(() => this.right.onCloseEmitter.fire(), () => { const leftWrite = new ArrrayBufferWriteBuffer(); leftWrite.onCommit(buffer => { - this.right.onMessageEmitter.fire(new ArrayBufferReadBuffer(buffer)); + this.right.onMessageEmitter.fire(new ArrayBufferReadBuffer(new Uint8Array(buffer))); }); return leftWrite; }); readonly right: ForwardingChannel = new ForwardingChannel(() => this.left.onCloseEmitter.fire(), () => { const rightWrite = new ArrrayBufferWriteBuffer(); rightWrite.onCommit(buffer => { - this.left.onMessageEmitter.fire(new ArrayBufferReadBuffer(buffer)); + this.left.onMessageEmitter.fire(new ArrayBufferReadBuffer(new Uint8Array(buffer))); }) return rightWrite; }); diff --git a/src/experiments.ts b/src/experiments.ts index 4f8c111..1729312 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -13,44 +13,128 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { ChannelPipe } from './channel'; -import { RpcHandler, RpcProxyHandler } from './rpc-proxy'; -import * as fs from 'fs'; +import { MessageDecoder, MessageEncoder } from './message-encoder'; +import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; /** * This file is for fiddling around and testing. Not production code. */ -const pipe = new ChannelPipe(); -interface ReadFile { - read(path: string): Promise; +//onst toEncode = ['string1', 'string2']; + +// Encode with stringify +//const stringified = JSON.stringify(toEncode); +//const encoded1 = Buffer.from(stringified); +//console.log(encoded1.byteLength); + +// then encoded1.byteLength= 21 + +// Encode with MessageEncoder +const encoder = new MessageEncoder(); +//const writer = new ArrrayBufferWriteBuffer(); +//encoder.writeTypedValue(writer, toEncode); +//const encoded2 = writer.getCurrentContents(); +//console.log(encoded2.byteLength); +// then encoded2.byteLength= 42 +// so factor 2 in this case + +const test1 = { + 'curve': 'yes', + 'successful': false, + 'does': [ + [ + 'tool', + 'strange', + 'declared', + false, + 'if', + false, + false, + true, + true, + 196639994 + ], + -1697924638.043861, + 1921422646, + 'hide', + false, + true, + true, + -400170969, + 550424783, + -2118374202.4598904 + ], + 'fish': 664495385.6069336, + 'eat': -1205575089, + 'boat': 1495629676, + 'arm': 'nation', + 'height': false, + 'underline': 'have', + 'satellites': -20686813.87966633 +}; + +const test2: unknown[] = []; +for (let index = 0; index < 100; index++) { + test2.push(test1); } -class Server implements ReadFile { - read(path: string): Promise { - const bytes = fs.readFileSync(path); - const result = new ArrayBuffer(bytes.byteLength); - bytes.copy(new Uint8Array(result)); - return Promise.resolve(result); - } +const test3: string[] = []; +for (let index = 0; index < 1000; index++) { + test3.push(`${index}`); + } -const handler = new RpcHandler(new Server()); -handler.onChannelOpen(pipe.right); -const proxyHandler = new RpcProxyHandler(); -const proxy: ReadFile = new Proxy(Object.create(null), proxyHandler); -proxyHandler.onChannelOpen(pipe.left); +time(10000, () => writeTypedValue(test2), 'New encoding of object'); +time(10000, () => writeTypedValue(JSON.stringify(test2)), 'Stringify of object'); + +function writeTypedValue(object: unknown) { + const writer = new ArrrayBufferWriteBuffer(); + encoder.writeTypedValue(writer, object); + writer.getCurrentContents(); +} + +function time(times: number, payload: () => void, name: string) { + const start2 = Date.now(); + for (let index = 0; index < times; index++) { + payload(); + // test(test1); + // test(test2); + // test(test3); + } + const end2 = Date.now(); + console.log(`${name} took ${end2 - start2} ms.`); +} + +export function test(object: unknown): void { + // console.log('Start test'); + const encoder = new MessageEncoder(); + const decoder = new MessageDecoder(); + // const string = fs.readFileSync(process.argv[2], 'utf8'); + // const object = JSON.parse(string); -const t0 = new Date().getTime(); + //const start1 = Date.now(); + const result = Buffer.from(JSON.stringify(object)); + //const end1 = Date.now(); + //console.log(`Stringify encoding of object took ${end1 - start1} ms. Final byte length: ${result.byteLength}`); -proxy.read(process.argv[2]).then(value => { - const t1 = new Date().getTime(); - console.log(`read file of length: ${value.byteLength} in ${t1 - t0}ms`); - console.log(value.slice(0, 20)); -}).catch(e => { - console.log(e); -}); + const writer = new ArrrayBufferWriteBuffer(); + //const start2 = Date.now(); + encoder.writeTypedValue(writer, object); + const result2 = writer.getCurrentContents(); + //const end2 = Date.now(); + //console.log(`New encoding of object took ${end2 - start2} ms. Final byte length: ${result2.byteLength}`); + //const start3 = Date.now(); + JSON.parse(result.toString()); + //const end3 = Date.now(); + //console.log(`Stringify Reading took ${end3 - start3} ms for`); + const reader = new ArrayBufferReadBuffer(result2); + //const start4 = Date.now(); + decoder.readTypedValue(reader); + //const end4 = Date.now(); + //console.log(`New Reading took ${end4 - start4} ms for`); + // console.log(); +} \ No newline at end of file diff --git a/src/message-buffer.ts b/src/message-buffer.ts index 1a99f5c..65b7d80 100644 --- a/src/message-buffer.ts +++ b/src/message-buffer.ts @@ -18,10 +18,12 @@ * A buffer maintaining a write position capable of writing primitive values */ export interface WriteBuffer { + writeLength(length: number): WriteBuffer writeByte(byte: number): WriteBuffer + writeNumber(value: number): WriteBuffer; writeInt(value: number): WriteBuffer; writeString(value: string): WriteBuffer; - writeBytes(value: ArrayBuffer): WriteBuffer; + writeBytes(value: Uint8Array): WriteBuffer; /** * Makes any writes to the buffer permanent, for example by sending the writes over a channel. @@ -33,11 +35,22 @@ export interface WriteBuffer { export class ForwardingWriteBuffer implements WriteBuffer { constructor(protected readonly underlying: WriteBuffer) { } + + writeLength(length: number): WriteBuffer { + this.underlying.writeLength(length); + return this; + } + writeByte(byte: number): WriteBuffer { this.underlying.writeByte(byte); return this; } + writeNumber(value: number): WriteBuffer { + this.underlying.writeNumber(value); + return this; + } + writeInt(value: number): WriteBuffer { this.underlying.writeInt(value); return this; @@ -48,7 +61,7 @@ export class ForwardingWriteBuffer implements WriteBuffer { return this; } - writeBytes(value: ArrayBuffer): WriteBuffer { + writeBytes(value: Uint8Array): WriteBuffer { this.underlying.writeBytes(value); return this; } @@ -63,8 +76,10 @@ export class ForwardingWriteBuffer implements WriteBuffer { * reading primitive values. */ export interface ReadBuffer { + readLength(): number; readByte(): number; + readNumber(): number; readInt(): number; readString(): string; - readBytes(): ArrayBuffer; + readBytes(): Uint8Array; } \ No newline at end of file diff --git a/src/message-encoder.spec.ts b/src/message-encoder.spec.ts index ad63a46..e94d463 100644 --- a/src/message-encoder.spec.ts +++ b/src/message-encoder.spec.ts @@ -19,7 +19,7 @@ import { MessageDecoder, MessageEncoder } from './message-encoder'; describe('message buffer test', () => { it('encode object', () => { - const buffer = new ArrayBuffer(1024); + const buffer = new Uint8Array(1024); const writer = new ArrrayBufferWriteBuffer(buffer); const encoder = new MessageEncoder(); diff --git a/src/message-encoder.ts b/src/message-encoder.ts index d208b61..6bb839b 100644 --- a/src/message-encoder.ts +++ b/src/message-encoder.ts @@ -76,7 +76,10 @@ enum ObjectType { ByteArray = 1, ObjectArray = 2, Undefined = 3, - Object = 4 + Object = 4, + String = 5, + Boolean = 6, + Number = 7 } /** * A value encoder writes javascript values to a write buffer. Encoders will be asked @@ -147,7 +150,7 @@ export class MessageDecoder { this.registerDecoder(ObjectType.Object, { read: (buf, recursiveRead) => { - const propertyCount = buf.readInt(); + const propertyCount = buf.readLength(); const result = Object.create({}); for (let i = 0; i < propertyCount; i++) { const key = buf.readString(); @@ -156,7 +159,24 @@ export class MessageDecoder { } return result; } + }); + this.registerDecoder(ObjectType.String, { + read: (buf, recursiveRead) => { + return buf.readString(); + } }) + + this.registerDecoder(ObjectType.Boolean, { + read: buf => { + return buf.readByte() === 1; + } + }); + + this.registerDecoder(ObjectType.Number, { + read: buf => { + return buf.readNumber(); + } + }); } registerDecoder(tag: number, decoder: ValueDecoder): void { @@ -256,7 +276,7 @@ export class MessageDecoder { } readArray(buf: ReadBuffer): any[] { - const length = buf.readInt(); + const length = buf.readLength(); const result = new Array(length); for (let i = 0; i < length; i++) { result[i] = this.readTypedValue(buf); @@ -265,7 +285,7 @@ export class MessageDecoder { } readTypedValue(buf: ReadBuffer): any { - const type = buf.readInt(); + const type = buf.readByte(); const decoder = this.decoders.get(type); if (!decoder) { throw new Error(`No decoder for tag ${type}`); @@ -292,6 +312,7 @@ export class MessageEncoder { buf.writeString(JSON.stringify(value)); } }); + this.registerEncoder(ObjectType.Object, { is: (value) => typeof value === 'object', write: (buf, object, recursiveEncode) => { @@ -304,7 +325,7 @@ export class MessageEncoder { } } - buf.writeInt(relevant.length); + buf.writeLength(relevant.length); for (const [property, value] of relevant) { buf.writeString(property); recursiveEncode(buf, value); @@ -329,6 +350,27 @@ export class MessageEncoder { buf.writeBytes(value); } }); + + this.registerEncoder(ObjectType.String, { + is: (value) => typeof value === 'string', + write: (buf, value) => { + buf.writeString(value); + } + }); + + this.registerEncoder(ObjectType.Boolean, { + is: (value) => typeof value === 'boolean', + write: (buf, value) => { + buf.writeByte(value === true ? 1 : 0); + } + }); + + this.registerEncoder(ObjectType.Number, { + is: (value) => typeof value === 'number', + write: (buf, value) => { + buf.writeNumber(value); + } + }); } registerEncoder(tag: number, encoder: ValueEncoder): void { @@ -373,7 +415,7 @@ export class MessageEncoder { writeTypedValue(buf: WriteBuffer, value: any): void { for (let i: number = this.encoders.length - 1; i >= 0; i--) { if (this.encoders[i][1].is(value)) { - buf.writeInt(this.encoders[i][0]); + buf.writeByte(this.encoders[i][0]); this.encoders[i][1].write(buf, value, (innerBuffer, innerValue) => { this.writeTypedValue(innerBuffer, innerValue); }); @@ -383,7 +425,7 @@ export class MessageEncoder { } writeArray(buf: WriteBuffer, value: any[]): void { - buf.writeInt(value.length); + buf.writeLength(value.length); for (let i = 0; i < value.length; i++) { this.writeTypedValue(buf, value[i]); } diff --git a/src/websocket-client-channel.ts b/src/websocket-client-channel.ts index 9385384..3ce1cf9 100644 --- a/src/websocket-client-channel.ts +++ b/src/websocket-client-channel.ts @@ -169,7 +169,7 @@ export class WebSocketClientChannel implements Channel { this.fireSocketDidOpen(); } const bytes = await response.arrayBuffer(); - this.onMessageEmitter.fire(new ArrayBufferReadBuffer(bytes)); + this.onMessageEmitter.fire(new ArrayBufferReadBuffer(new Uint8Array(bytes))); } else { timeoutDuration = this.httpFallbackOptions?.errorTimeout || 0; this.httpFallbackDisconnected = true;