Skip to content

Commit

Permalink
[JavaScript] Implement the standard protocol (#1286)
Browse files Browse the repository at this point in the history
1. Make the tag string lazy to avoid creating strings.
2. Add the serializeVolatile API to take ownership of the bufferWriter
and avoid buffer copying, which is 30% faster than the serialize
operation. There is a usage limitation; the bufferWriter remains
inaccessible until the dispose method from the result of
serializeVolatile has been called.
4. Remove the useLatin1 configuration, which is a standard in the Fury
protocol.

Before: 

![image](https://github.com/apache/incubator-fury/assets/16490211/aa8cba42-aafd-4f5b-ab73-2ac698dc8726)

After:

![image](https://github.com/apache/incubator-fury/assets/16490211/106ea85c-be7d-4bfe-98b1-05d766b43035)
  • Loading branch information
theweipeng authored Jan 2, 2024
1 parent c1c24e4 commit 20f1849
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 101 deletions.
2 changes: 1 addition & 1 deletion javascript/benchmark/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
const Fury = require("@furyjs/fury");
const utils = require("../test/util");
const hps = require('@furyjs/hps');
const fury = new Fury.default({ hps, refTracking: false, useLatin1: true, useSliceString: true });
const fury = new Fury.default({ hps, refTracking: false, useSliceString: true });
const Benchmark = require("benchmark");
const protobuf = require("protobufjs");
const path = require('path');
Expand Down
Binary file added javascript/benchmark/sample.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions javascript/packages/fury/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,19 @@ export default class {
serialize: (data: ToRecordType<T>) => {
return this.fury.serialize(data, serializer);
},
serializeVolatile: (data: ToRecordType<T>) => {
return this.fury.serializeVolatile(data, serializer);
},
deserialize: (bytes: Uint8Array) => {
return this.fury.deserialize(bytes, serializer) as ToRecordType<T>;
},
};
}

serializeVolatile(v: any, serialize?: Serializer) {
return this.fury.serializeVolatile(v, serialize);
}

serialize(v: any, serialize?: Serializer) {
return this.fury.serialize(v, serialize);
}
Expand Down
50 changes: 42 additions & 8 deletions javascript/packages/fury/lib/classResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,39 @@ import { BinaryWriter } from "./writer";
const USESTRINGVALUE = 0;
const USESTRINGID = 1;

class Lazystring {
private string: string | null = null;
private start: number | null = null;
private len: number | null = null;

static fromPair(start: number, len: number) {
const result = new Lazystring();
result.start = start;
result.len = len;
return result;
}

static fromString(str: string) {
const result = new Lazystring();
result.string = str;
return result;
}

toString(binaryReader: BinaryReader) {
if (this.string == null) {
const str = binaryReader.stringUtf8At(this.start!, this.len!);
return str;
}
return this.string;
}
}

export default class SerializerResolver {
private internalSerializer: Serializer[] = new Array(300);
private customSerializer: { [key: string]: Serializer } = {
};

private readStringPool: string[] = [];
private readStringPool: Lazystring[] = [];
private writeStringCount = 0;
private writeStringIndex: number[] = [];

Expand Down Expand Up @@ -139,22 +166,29 @@ export default class SerializerResolver {
const flag = binaryReader.uint8();
if (flag === USESTRINGVALUE) {
binaryReader.skip(8); // The tag hash is not needed at the moment.
const str = binaryReader.stringUtf8(binaryReader.int16());
return str;
return binaryReader.stringUtf8(binaryReader.int16());
} else {
return this.readStringPool[binaryReader.int16()];
return this.readStringPool[binaryReader.int16()].toString(binaryReader);
}
}

readTag(binaryReader: BinaryReader) {
const flag = binaryReader.uint8();
if (flag === USESTRINGVALUE) {
binaryReader.skip(8); // The tag hash is not needed at the moment.
const str = binaryReader.stringUtf8(binaryReader.int16());
this.readStringPool.push(str);
return str;
const start = binaryReader.getCursor();
const len = binaryReader.int16();
binaryReader.skip(len);
this.readStringPool.push(Lazystring.fromPair(start, len));
const idx = this.readStringPool.length;
return () => {
return this.readStringPool[idx - 1].toString(binaryReader);
};
} else {
return this.readStringPool[binaryReader.int16()];
const idx = binaryReader.int16();
return () => {
return this.readStringPool[idx].toString(binaryReader);
};
}
}
}
28 changes: 28 additions & 0 deletions javascript/packages/fury/lib/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export class OwnershipError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
24 changes: 21 additions & 3 deletions javascript/packages/fury/lib/fury.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { BinaryWriter } from "./writer";
import { BinaryReader } from "./reader";
import { ReferenceResolver } from "./referenceResolver";
import { ConfigFlags, Serializer, Config, InternalSerializerType, Language } from "./type";
import { OwnershipError } from "./error";

export default (config: Config) => {
const binaryReader = BinaryReader(config);
Expand All @@ -38,6 +39,7 @@ export default (config: Config) => {
classResolver,
binaryReader,
binaryWriter,
serializeVolatile,
};
classResolver.init(fury);

Expand Down Expand Up @@ -71,10 +73,17 @@ export default (config: Config) => {
}
}

function serialize<T = any>(data: T, serializer?: Serializer) {
function serializeInternal<T = any>(data: T, serializer?: Serializer) {
try {
binaryWriter.reset();
} catch (e) {
if (e instanceof OwnershipError) {
throw new Error("Permission denied. To release the serialization ownership, you must call the dispose function returned by serializeVolatile.");
}
throw e;
}
referenceResolver.reset();
classResolver.reset();
binaryWriter.reset();
let bitmap = 0;
if (data === null) {
bitmap |= ConfigFlags.isNullFlag;
Expand All @@ -92,7 +101,16 @@ export default (config: Config) => {
classResolver.getSerializerById(InternalSerializerType.ANY).write(data);
}
binaryWriter.setUint32Position(cursor, binaryWriter.getCursor()); // nativeObjects start offsets;
return binaryWriter.dump();
return binaryWriter;
}

function serialize<T = any>(data: T, serializer?: Serializer) {
return serializeInternal(data, serializer).dump();
}

function serializeVolatile<T = any>(data: T, serializer?: Serializer) {
return serializeInternal(data, serializer).dumpAndOwn();
}

return fury;
};
8 changes: 4 additions & 4 deletions javascript/packages/fury/lib/internalSerializer/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { InternalSerializerType, RefFlags, Fury } from "../type";

export const uInt8Serializer = (fury: Fury) => {
const { binaryWriter, binaryReader, referenceResolver } = fury;
const { int8: writeInt8, uint8: writeUInt8 } = binaryWriter;
const { uint8: writeUInt8 } = binaryWriter;
const { uint8: readUInt8 } = binaryReader;
return {
...referenceResolver.deref(() => {
Expand Down Expand Up @@ -107,7 +107,7 @@ export const int8Serializer = (fury: Fury) => {

export const uInt16Serializer = (fury: Fury) => {
const { binaryWriter, binaryReader, referenceResolver } = fury;
const { int8: writeInt8, uint16: writeUInt16 } = binaryWriter;
const { uint16: writeUInt16 } = binaryWriter;
const { uint16: readUInt16 } = binaryReader;

return {
Expand Down Expand Up @@ -151,7 +151,7 @@ export const int16Serializer = (fury: Fury) => {

export const uInt32Serializer = (fury: Fury) => {
const { binaryWriter, binaryReader, referenceResolver } = fury;
const { int8: writeInt8, uint32: writeUInt32 } = binaryWriter;
const { uint32: writeUInt32 } = binaryWriter;
const { uint32: readUInt32 } = binaryReader;

return {
Expand Down Expand Up @@ -195,7 +195,7 @@ export const int32Serializer = (fury: Fury) => {

export const uInt64Serializer = (fury: Fury) => {
const { binaryWriter, binaryReader, referenceResolver } = fury;
const { int8: writeInt8, uint64: writeUInt64 } = binaryWriter;
const { uint64: writeUInt64 } = binaryWriter;
const { uint64: readUInt64 } = binaryReader;

return {
Expand Down
9 changes: 7 additions & 2 deletions javascript/packages/fury/lib/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,20 @@ export const BinaryReader = (config: Config) => {
return result;
}

function stringUtf8At(start: number, len: number) {
return buffer.utf8Slice(start, start + len);
}

function stringUtf8(len: number) {
const result = buffer.utf8Slice(cursor, cursor + len);
cursor += len;
return result;
}

function stringOfVarInt32() {
const useLatin1 = config.useLatin1 ? uint8() === LATIN1 : false;
const isLatin1 = uint8() === LATIN1;
const len = varInt32();
return useLatin1 ? stringLatin1(len) : stringUtf8(len);
return isLatin1 ? stringLatin1(len) : stringUtf8(len);
}

function stringLatin1Fast(len: number) {
Expand Down Expand Up @@ -167,6 +171,7 @@ export const BinaryReader = (config: Config) => {
bufferRef,
uint8,
reset,
stringUtf8At,
stringUtf8,
stringLatin1,
stringOfVarInt32,
Expand Down
1 change: 0 additions & 1 deletion javascript/packages/fury/lib/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export interface Hps {
export interface Config {
hps?: Hps
refTracking?: boolean
useLatin1?: boolean
useSliceString?: boolean
}

Expand Down
26 changes: 20 additions & 6 deletions javascript/packages/fury/lib/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { Config, LATIN1, UTF8 } from "./type";
import { PlatformBuffer, alloc, strByteLength } from "./platformBuffer";
import { OwnershipError } from "./error";

const MAX_POOL_SIZE = 1024 * 1024 * 3; // 3MB

Expand All @@ -28,6 +29,7 @@ export const BinaryWriter = (config: Config) => {
let arrayBuffer: PlatformBuffer;
let dataView: DataView;
let reserved = 0;
let locked = false;

function initPoll() {
byteLength = 1024 * 100;
Expand All @@ -49,6 +51,9 @@ export const BinaryWriter = (config: Config) => {
}

function reset() {
if (locked) {
throw new OwnershipError("Ownership of writer was held by dumpAndOwn, but not released");
}
cursor = 0;
reserved = 0;
}
Expand Down Expand Up @@ -170,9 +175,7 @@ export const BinaryWriter = (config: Config) => {
return function (v: string) {
const isLatin1 = detectIsLatin1(v);
const len = isLatin1 ? v.length : strByteLength(v);
if (config.useLatin1) {
dataView.setUint8(cursor++, isLatin1 ? LATIN1 : UTF8);
}
dataView.setUint8(cursor++, isLatin1 ? LATIN1 : UTF8);
varInt32(len);
reserve(len);
if (isLatin1) {
Expand All @@ -191,9 +194,7 @@ export const BinaryWriter = (config: Config) => {
function stringOfVarInt32Slow(v: string) {
const len = strByteLength(v);
const isLatin1 = len === v.length;
if (config.useLatin1) {
dataView.setUint8(cursor++, isLatin1 ? LATIN1 : UTF8);
}
dataView.setUint8(cursor++, isLatin1 ? LATIN1 : UTF8);
varInt32(len);
reserve(len);
if (isLatin1) {
Expand Down Expand Up @@ -236,6 +237,18 @@ export const BinaryWriter = (config: Config) => {
return result;
}

function dumpAndOwn() {
locked = true;
return {
get() {
return arrayBuffer.subarray(0, cursor);
},
dispose() {
locked = false;
},
};
}

function getCursor() {
return cursor;
}
Expand Down Expand Up @@ -278,5 +291,6 @@ export const BinaryWriter = (config: Config) => {
int32,
getCursor,
setUint32Position,
dumpAndOwn,
};
};
2 changes: 1 addition & 1 deletion javascript/test/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('array', () => {
}
};

const fury = new Fury({ refTracking: true, useLatin1: true });
const fury = new Fury({ refTracking: true });
const serializer = fury.registerSerializer(description).serializer;
const input = fury.serialize({
a7: ["hello", "world", null]
Expand Down
Loading

0 comments on commit 20f1849

Please sign in to comment.