blob: 15e6f3e44098b1a9cbab97adf1d846789070a07a [file]
/*
* 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.
*/
import Fory, { Type } from "../packages/core/index";
import { describe, expect, test } from "@jest/globals";
const DEFAULT_GRAPH_MEMORY_BYTES = 128 * 1024 * 1024;
const REFERENCE_BYTES = 4;
const STRUCT_OBJECT_OWNER_BYTES = 6 * REFERENCE_BYTES;
const ARRAY_LIST_OWNER_BYTES = 6 * REFERENCE_BYTES;
const JS_MAP_OWNER_BYTES = 8 * REFERENCE_BYTES;
const objectBytes = (fields: number) => STRUCT_OBJECT_OWNER_BYTES + fields * REFERENCE_BYTES;
const listBytes = (count: number) => ARRAY_LIST_OWNER_BYTES + count * REFERENCE_BYTES;
const mapBytes = (count: number) => JS_MAP_OWNER_BYTES + count * 2 * REFERENCE_BYTES;
function serializeAny(value: unknown) {
return new Fory({ compatible: false, ref: true }).serialize(value);
}
function deserializeAny(bytes: Uint8Array, maxGraphMemoryBytes: number) {
return new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes,
}).deserialize(bytes);
}
describe("graph memory budget", () => {
test("uses fixed default budget", () => {
const fory = new Fory({ compatible: false });
fory.readContext.reset(new Uint8Array(17));
expect(() => fory.readContext.reserveGraphMemory(DEFAULT_GRAPH_MEMORY_BYTES)).not.toThrow();
expect(() => fory.readContext.reserveGraphMemory(1)).toThrow(/maxGraphMemoryBytes/);
});
test("handles explicit config and validation", () => {
const fory = new Fory({ maxGraphMemoryBytes: 24 });
fory.readContext.reset(new Uint8Array(1));
expect(() => fory.readContext.reserveGraphMemory(0)).not.toThrow();
expect(() => fory.readContext.reserveGraphMemory(24)).not.toThrow();
expect(() => fory.readContext.reserveGraphMemory(1)).toThrow(/maxGraphMemoryBytes/);
expect(() => new Fory({ maxGraphMemoryBytes: 0 })).toThrow(/maxGraphMemoryBytes/);
expect(() => new Fory({ maxGraphMemoryBytes: -2 })).toThrow(/maxGraphMemoryBytes/);
});
test("uses parent storage for nested empty containers", () => {
const typeInfo = Type.struct("budget.nested.empty", {
values: Type.list(Type.list(Type.int32({ encoding: "fixed" }))).setId(1),
});
const writer = new Fory({ compatible: false, ref: true });
const bytes = writer.register(typeInfo).serialize({ values: [[]] });
const passingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(1) + listBytes(1) + listBytes(0),
}).register(typeInfo);
const failingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(1) + listBytes(1) + listBytes(0) - 1,
}).register(typeInfo);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
expect(passingReader.deserialize(bytes)).toEqual({ values: [[]] });
});
test("reserves sibling containers cumulatively", () => {
const typeInfo = Type.struct("budget.sibling.empty", {
values: Type.list(Type.list(Type.int32({ encoding: "fixed" }))).setId(1),
});
const writer = new Fory({ compatible: false, ref: true });
const bytes = writer.register(typeInfo).serialize({
values: [[], [], []],
});
const passingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(1) + listBytes(3) + 3 * listBytes(0),
}).register(typeInfo);
const failingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(1) + listBytes(3) + 3 * listBytes(0) - 1,
}).register(typeInfo);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
expect(passingReader.deserialize(bytes)).toEqual({
values: [[], [], []],
});
});
test("reserves empty generated object owner", () => {
const childType = Type.struct("budget.empty.object", {});
const typeInfo = Type.struct("budget.empty.parent", {
first: Type.struct("budget.empty.object").setId(1),
second: Type.struct("budget.empty.object").setId(2),
});
const writer = new Fory({ compatible: false, ref: true });
writer.register(childType);
const bytes = writer.register(typeInfo).serialize({
first: {},
second: {},
});
const required = objectBytes(2) + 2 * STRUCT_OBJECT_OWNER_BYTES;
const passingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: required,
});
passingReader.register(childType);
passingReader.register(typeInfo);
const failingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: required - 1,
});
failingReader.register(childType);
failingReader.register(typeInfo);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
expect(passingReader.deserialize(bytes)).toEqual({
first: {},
second: {},
});
});
test("preserves constructor for empty generated object owner", () => {
class EmptyChild {}
Type.struct("budget.empty.ctor.child", {})(EmptyChild);
class EmptyParent {
child = new EmptyChild();
}
Type.struct("budget.empty.ctor.parent", {
child: Type.struct("budget.empty.ctor.child").setId(1),
})(EmptyParent);
const writer = new Fory({ compatible: false, ref: true });
writer.register(EmptyChild);
const bytes = writer.register(EmptyParent).serialize(new EmptyParent());
const required = objectBytes(1) + STRUCT_OBJECT_OWNER_BYTES;
const passingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: required,
});
passingReader.register(EmptyChild);
passingReader.register(EmptyParent);
const failingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: required - 1,
});
failingReader.register(EmptyChild);
failingReader.register(EmptyParent);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
const decoded = passingReader.deserialize(bytes);
expect(decoded).toBeInstanceOf(EmptyParent);
expect(decoded.child).toBeInstanceOf(EmptyChild);
});
test("reserves generated self reference once", () => {
const typeInfo = Type.struct("budget.self.node", {
id: Type.int32({ encoding: "fixed" }).setId(1),
next: Type.struct("budget.self.node").setId(2).setNullable(true).setTrackingRef(true),
children: Type.list(Type.struct("budget.self.node").setTrackingRef(true)).setId(3),
});
const node: any = { id: 7, next: null, children: [] };
node.next = node;
node.children = [node];
const writer = new Fory({ compatible: false, ref: true });
const bytes = writer.register(typeInfo).serialize(node);
const required = objectBytes(3) + listBytes(1);
const passingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: required,
}).register(typeInfo);
const failingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: required - 1,
}).register(typeInfo);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
const decoded: any = passingReader.deserialize(bytes);
expect(decoded.id).toBe(7);
expect(decoded.next).toBe(decoded);
expect(decoded.children).toHaveLength(1);
expect(decoded.children[0]).toBe(decoded);
});
test("reserves map entries", () => {
const bytes = serializeAny(new Map([[1, 2]]));
expect(() => deserializeAny(bytes, mapBytes(1) - 1)).toThrow(/maxGraphMemoryBytes/);
expect(deserializeAny(bytes, mapBytes(1))).toEqual(new Map([[1, 2]]));
});
test("reserves generated containers", () => {
const typeInfo = Type.struct("budget.generated", {
list: Type.list(Type.int32({ encoding: "fixed" })).setId(1),
set: Type.set(Type.string()).setId(2),
map: Type.map(Type.string(), Type.int32({ encoding: "fixed" })).setId(3),
});
const writer = new Fory({ compatible: false, ref: true });
const bytes = writer.register(typeInfo).serialize({
list: [1],
set: new Set(["a"]),
map: new Map([["k", 1]]),
});
const passingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(3) + listBytes(1) + listBytes(1) + mapBytes(1),
}).register(typeInfo);
const failingReader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(3) + listBytes(1) + listBytes(1) + mapBytes(1) - 1,
}).register(typeInfo);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
expect(passingReader.deserialize(bytes)).toEqual({
list: [1],
set: new Set(["a"]),
map: new Map([["k", 1]]),
});
});
test("skips compatible list to typed array leaf", () => {
const writerType = Type.struct(9010, {
values: Type.list(Type.int32({ encoding: "fixed" })).setId(1),
});
const readerType = Type.struct(9010, {
values: Type.int32Array().setId(1),
});
const writer = new Fory({ compatible: true });
const bytes = writer.register(writerType).serialize({ values: [1, 2, 3] });
const passingReader = new Fory({
compatible: true,
maxGraphMemoryBytes: objectBytes(1),
}).register(readerType);
const failingReader = new Fory({
compatible: true,
maxGraphMemoryBytes: objectBytes(1) - 1,
}).register(readerType);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
expect(Array.from(passingReader.deserialize(bytes).values)).toEqual([1, 2, 3]);
});
test("charges compatible typed array to list materialization", () => {
const writerType = Type.struct(9011, {
values: Type.int32Array().setId(1),
});
const readerType = Type.struct(9011, {
values: Type.list(Type.int32({ encoding: "fixed" })).setId(1),
});
const writer = new Fory({ compatible: true });
const bytes = writer.register(writerType).serialize({
values: new Int32Array([1, 2, 3]),
});
const required = objectBytes(1) + listBytes(3);
const passingReader = new Fory({
compatible: true,
maxGraphMemoryBytes: required,
}).register(readerType);
const failingReader = new Fory({
compatible: true,
maxGraphMemoryBytes: required - 1,
}).register(readerType);
expect(() => failingReader.deserialize(bytes)).toThrow(/maxGraphMemoryBytes/);
expect(passingReader.deserialize(bytes)).toEqual({ values: [1, 2, 3] });
});
test("skips scalar dense owners", () => {
const typeInfo = Type.struct("budget.skipped", {
text: Type.string().setId(1),
binary: Type.binary().setId(2),
values: Type.int32Array().setId(3),
});
const writer = new Fory({ compatible: false, ref: true });
const bytes = writer.register(typeInfo).serialize({
text: "hello",
binary: new Uint8Array([1, 2, 3]),
values: new Int32Array([1, 2, 3]),
});
const reader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: objectBytes(3),
}).register(typeInfo);
expect(reader.deserialize(bytes)).toEqual({
text: "hello",
binary: new Uint8Array([1, 2, 3]),
values: new Int32Array([1, 2, 3]),
});
});
test("keeps byte checks", () => {
const typeInfo = Type.struct("budget.bytecheck", {
values: Type.int32Array().setId(1),
});
const writer = new Fory({ compatible: false, ref: true });
const bytes = writer.register(typeInfo).serialize({
values: new Int32Array([1, 2, 3]),
});
const reader = new Fory({
compatible: false,
ref: true,
maxGraphMemoryBytes: 1024 * 1024,
}).register(typeInfo);
expect(() => reader.deserialize(bytes.slice(0, bytes.length - 1))).toThrow();
});
});