| /* |
| * 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(); |
| }); |
| }); |