Skip to content

Commit

Permalink
Merge pull request #4 from flora-suite/feature/upstream
Browse files Browse the repository at this point in the history
Fix toggling of panels by listen only to revert event
  • Loading branch information
vincentdji authored Nov 15, 2024
2 parents 67b4b6c + 7849537 commit abff231
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 83 deletions.
32 changes: 32 additions & 0 deletions packages/suite-base/src/players/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

/**
* Values of the contants below are a (more or less) informed guesses and not guaranteed to be accurate.
*/
export const COMPRESSED_POINTER_SIZE = 4; // Pointers use 4 bytes (also on 64-bit systems) due to pointer compression
export const OBJECT_BASE_SIZE = 3 * COMPRESSED_POINTER_SIZE; // 3 compressed pointers
// Arrays have an additional length property (1 pointer) and a backing store header (2 pointers)
// See https://stackoverflow.com/a/70550693.
export const ARRAY_BASE_SIZE = OBJECT_BASE_SIZE + 3 * COMPRESSED_POINTER_SIZE;
export const TYPED_ARRAY_BASE_SIZE = 25 * COMPRESSED_POINTER_SIZE; // byteLength, byteOffset, ..., see https://stackoverflow.com/a/45808835
export const SMALL_INTEGER_SIZE = COMPRESSED_POINTER_SIZE; // Small integers (up to 31 bits), pointer tagging
export const HEAP_NUMBER_SIZE = 8 + 2 * COMPRESSED_POINTER_SIZE; // 4-byte map pointer + 8-byte payload + property pointer
export const FIELD_SIZE_BY_PRIMITIVE: Record<string, number> = {
bool: SMALL_INTEGER_SIZE,
int8: SMALL_INTEGER_SIZE,
uint8: SMALL_INTEGER_SIZE,
int16: SMALL_INTEGER_SIZE,
uint16: SMALL_INTEGER_SIZE,
int32: SMALL_INTEGER_SIZE,
uint32: SMALL_INTEGER_SIZE,
float32: HEAP_NUMBER_SIZE,
float64: HEAP_NUMBER_SIZE,
int64: HEAP_NUMBER_SIZE,
uint64: HEAP_NUMBER_SIZE,
time: OBJECT_BASE_SIZE + 2 * HEAP_NUMBER_SIZE + COMPRESSED_POINTER_SIZE,
duration: OBJECT_BASE_SIZE + 2 * HEAP_NUMBER_SIZE + COMPRESSED_POINTER_SIZE,
string: 20, // we don't know the length upfront, assume a fixed length
};
export const MAX_NUM_FAST_PROPERTIES = 1020;
25 changes: 24 additions & 1 deletion packages/suite-base/src/players/messageMemoryEstimation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { OBJECT_BASE_SIZE } from "./constants";
import {
estimateMessageObjectSize,
estimateMessageFieldSizes,
OBJECT_BASE_SIZE,
estimateObjectSize,
} from "./messageMemoryEstimation";

Expand Down Expand Up @@ -145,6 +145,29 @@ describe("memoryEstimationByObject", () => {
expect(sizeInBytes).toBeGreaterThan(0);
});

it("correctly estimates the size for a large object with more than 1020 fields", () => {
const largeObject: Record<string, unknown> = {};
const numProps = 1021;
const propertiesDictSize = 24632;
let valueSize = 0;

for (let i = 0; i < numProps; i++) {
if (i % 3 === 0) {
largeObject[`field${i}`] = i;
valueSize += 4;
} else if (i % 3 === 1) {
largeObject[`field${i}`] = true;
valueSize += 4;
} else {
largeObject[`field${i}`] = 1.23;
valueSize += 16;
}
}
const expectedSize = 12 + valueSize + propertiesDictSize - numProps * 4;
const sizeInBytes = estimateObjectSize(largeObject);
expect(sizeInBytes).toEqual(expectedSize);
});

it("correctly estimates the size for a simple object", () => {
const sizeInBytes = estimateObjectSize({
field1: 1, // 4 bytes, SMI (fits in pointer)
Expand Down
156 changes: 79 additions & 77 deletions packages/suite-base/src/players/messageMemoryEstimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,18 @@
import Log from "@lichtblick/log";
import { MessageDefinitionMap } from "@lichtblick/mcap-support/src/types";

import {
COMPRESSED_POINTER_SIZE,
OBJECT_BASE_SIZE,
ARRAY_BASE_SIZE,
TYPED_ARRAY_BASE_SIZE,
SMALL_INTEGER_SIZE,
HEAP_NUMBER_SIZE,
FIELD_SIZE_BY_PRIMITIVE,
MAX_NUM_FAST_PROPERTIES,
} from "./constants";

const log = Log.getLogger(__filename);
/**
* Values of the contants below are a (more or less) informed guesses and not guaranteed to be accurate.
*/
const COMPRESSED_POINTER_SIZE = 4; // Pointers use 4 bytes (also on 64-bit systems) due to pointer compression
export const OBJECT_BASE_SIZE = 3 * COMPRESSED_POINTER_SIZE; // 3 compressed pointers
// Arrays have an additional length property (1 pointer) and a backing store header (2 pointers)
// See https://stackoverflow.com/a/70550693.
const ARRAY_BASE_SIZE = OBJECT_BASE_SIZE + 3 * COMPRESSED_POINTER_SIZE;
const TYPED_ARRAY_BASE_SIZE = 25 * COMPRESSED_POINTER_SIZE; // byteLength, byteOffset, ..., see https://stackoverflow.com/a/45808835
const SMALL_INTEGER_SIZE = COMPRESSED_POINTER_SIZE; // Small integers (up to 31 bits), pointer tagging
const HEAP_NUMBER_SIZE = 8 + 2 * COMPRESSED_POINTER_SIZE; // 4-byte map pointer + 8-byte payload + property pointer
const FIELD_SIZE_BY_PRIMITIVE: Record<string, number> = {
bool: SMALL_INTEGER_SIZE,
int8: SMALL_INTEGER_SIZE,
uint8: SMALL_INTEGER_SIZE,
int16: SMALL_INTEGER_SIZE,
uint16: SMALL_INTEGER_SIZE,
int32: SMALL_INTEGER_SIZE,
uint32: SMALL_INTEGER_SIZE,
float32: HEAP_NUMBER_SIZE,
float64: HEAP_NUMBER_SIZE,
int64: HEAP_NUMBER_SIZE,
uint64: HEAP_NUMBER_SIZE,
time: OBJECT_BASE_SIZE + 2 * HEAP_NUMBER_SIZE + COMPRESSED_POINTER_SIZE,
duration: OBJECT_BASE_SIZE + 2 * HEAP_NUMBER_SIZE + COMPRESSED_POINTER_SIZE,
string: 20, // we don't know the length upfront, assume a fixed length
};
const MAX_NUM_FAST_PROPERTIES = 1020;

/**
* Estimates the memory size of a deserialized message object based on the schema definition.
Expand Down Expand Up @@ -203,69 +186,88 @@ export function estimateObjectSize(obj: unknown): number {
if (obj == undefined) {
return SMALL_INTEGER_SIZE;
}

const estimateArraySize = (array: unknown[]): number =>
COMPRESSED_POINTER_SIZE +
ARRAY_BASE_SIZE +
array.reduce(
(accumulator: number, value: unknown) => accumulator + estimateObjectSize(value),
0,
);

const estimateMapSize = (map: Map<unknown, unknown>): number =>
COMPRESSED_POINTER_SIZE +
OBJECT_BASE_SIZE +
Array.from(map.entries()).reduce(
(accumulator: number, [key, value]: [unknown, unknown]) =>
accumulator + estimateObjectSize(key) + estimateObjectSize(value),
0,
);

const estimateSetSize = (set: Set<unknown>): number =>
COMPRESSED_POINTER_SIZE +
OBJECT_BASE_SIZE +
Array.from(set.values()).reduce(
(accumulator: number, value: unknown) => accumulator + estimateObjectSize(value),
0,
);

const estimateObjectPropertiesSize = (object: Record<string, unknown>): number => {
const valuesSize = Object.values(object).reduce(
(accumulator: number, value: unknown) => accumulator + estimateObjectSize(value),
0,
);
const numProps = Object.keys(obj).length;

if (numProps > MAX_NUM_FAST_PROPERTIES) {
// If there are too many properties, V8 stores Objects in dictionary mode (slow properties)
// with each object having a self-contained dictionary. This dictionary contains the key, value
// and details of properties. Below we estimate the size of this additional dictionary. Formula
// adapted from medium.com/@bpmxmqd/v8-engine-jsobject-structure-analysis-and-memory-optimization-ideas-be30cfcdcd16
const propertiesDictSize =
16 + 5 * 8 + 2 ** Math.ceil(Math.log2((numProps + 2) * 1.5)) * 3 * 4;
return (
OBJECT_BASE_SIZE + valuesSize + propertiesDictSize - numProps * COMPRESSED_POINTER_SIZE
);
}

return OBJECT_BASE_SIZE + valuesSize;
};

switch (typeof obj) {
case "undefined":
case "boolean": {
case "boolean":
return SMALL_INTEGER_SIZE;
}
case "number": {

case "number":
return Number.isInteger(obj) ? SMALL_INTEGER_SIZE : HEAP_NUMBER_SIZE;
}
case "bigint": {

case "bigint":
return HEAP_NUMBER_SIZE;
}
case "string": {
// The string length is rounded up to the next multiple of 4.

case "string":
return COMPRESSED_POINTER_SIZE + OBJECT_BASE_SIZE + Math.ceil(obj.length / 4) * 4;
}
case "object": {

case "object":
if (Array.isArray(obj)) {
return (
COMPRESSED_POINTER_SIZE +
ARRAY_BASE_SIZE +
Object.values(obj).reduce((acc, val) => acc + estimateObjectSize(val), 0)
);
} else if (ArrayBuffer.isView(obj)) {
return estimateArraySize(obj);
}
if (ArrayBuffer.isView(obj)) {
return TYPED_ARRAY_BASE_SIZE + obj.byteLength;
} else if (obj instanceof Set) {
return (
COMPRESSED_POINTER_SIZE +
OBJECT_BASE_SIZE +
Array.from(obj.values()).reduce((acc, val) => acc + estimateObjectSize(val), 0)
);
} else if (obj instanceof Map) {
return (
COMPRESSED_POINTER_SIZE +
OBJECT_BASE_SIZE +
Array.from(obj.entries()).reduce(
(acc, [key, val]) => acc + estimateObjectSize(key) + estimateObjectSize(val),
0,
)
);
}

let propertiesSize = 0;
const numProps = Object.keys(obj).length;
if (numProps > MAX_NUM_FAST_PROPERTIES) {
// If there are too many properties, V8 stores Objects in dictionary mode (slow properties)
// with each object having a self-contained dictionary. This dictionary contains the key, value
// and details of properties. Below we estimate the size of this additional dictionary. Formula
// adapted from
// medium.com/@bpmxmqd/v8-engine-jsobject-structure-analysis-and-memory-optimization-ideas-be30cfcdcd16
const propertiesDictSize =
16 + 5 * 8 + 2 ** Math.ceil(Math.log2((numProps + 2) * 1.5)) * 3 * 4;
// In return, properties are no longer stored in the properties array, so we subtract that.
propertiesSize = propertiesDictSize - numProps * COMPRESSED_POINTER_SIZE;
if (obj instanceof Set) {
return estimateSetSize(obj);
}
if (obj instanceof Map) {
return estimateMapSize(obj);
}
return estimateObjectPropertiesSize(obj as Record<string, unknown>);

const valuesSize = Object.values(obj).reduce((acc, val) => acc + estimateObjectSize(val), 0);
return OBJECT_BASE_SIZE + propertiesSize + valuesSize;
}
case "symbol":
case "function": {
case "function":
throw new Error(`Can't estimate size of type '${typeof obj}'`);
}
}

log.error(`Can't estimate size of type '${typeof obj}'`);
return SMALL_INTEGER_SIZE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,16 @@ export default function CurrentLayoutProvider({
[setLayoutState],
);

// Changes to the layout storage from external user actions (such as resetting a layout to a
// previous saved state) need to trigger setLayoutState.
/**
* Changes to the layout storage from external user actions need to trigger setLayoutState.
* Before it was beeing trigged on every change. Now it is triggered only when the layout
* is reverted, otherize it has some toggling issues when resizing panels.
*/
useEffect(() => {
const listener: LayoutManagerEventTypes["change"] = ({ updatedLayout }) => {
const listener: LayoutManagerEventTypes["change"] = (event) => {
const { updatedLayout } = event;
if (
event.type === "revert" &&
updatedLayout &&
layoutStateRef.current.selectedLayout &&
updatedLayout.id === layoutStateRef.current.selectedLayout.id
Expand Down
2 changes: 1 addition & 1 deletion packages/suite-base/src/services/ILayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Layout, LayoutPermission } from "@lichtblick/suite-base/services/ILayou

export type LayoutManagerChangeEvent =
| { type: "delete"; updatedLayout?: undefined; layoutId: LayoutID }
| { type: "change"; updatedLayout: Layout | undefined };
| { type: "change" | "revert"; updatedLayout: Layout | undefined };

export type LayoutManagerEventTypes = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export default class LayoutManager implements ILayoutManager {
working: undefined,
});
});
this.#notifyChangeListeners({ type: "change", updatedLayout: result });
this.#notifyChangeListeners({ type: "revert", updatedLayout: result });
return result;
}

Expand Down
28 changes: 28 additions & 0 deletions packages/suite-base/src/testing/builders/MessageEventBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { MessageEvent } from "@lichtblick/suite";
import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder";
import RosTimeBuilder from "@lichtblick/suite-base/testing/builders/RosTimeBuilder";
import { defaults } from "@lichtblick/suite-base/testing/builders/utilities";

class MessageEventBuilder {
public static messageEvent<T>(props: Partial<MessageEvent<T>> = {}): MessageEvent<T> {
return defaults<MessageEvent<T>>(props, {
message: BasicBuilder.stringMap() as T,
publishTime: RosTimeBuilder.time(),
receiveTime: RosTimeBuilder.time(),
schemaName: BasicBuilder.string(),
sizeInBytes: BasicBuilder.number(),
topic: BasicBuilder.string(),
topicConfig: BasicBuilder.stringMap(),
});
}

public static messageEvents(count = 3): MessageEvent[] {
return BasicBuilder.multiple(MessageEventBuilder.messageEvent, count);
}
}

export default MessageEventBuilder;

0 comments on commit abff231

Please sign in to comment.