Skip to content

Commit

Permalink
Support BigInt values
Browse files Browse the repository at this point in the history
JSON doesn't contrain numeric values to be specific sizes. In most
languages this isn't an issue thanks to proper support for a wide
variety of number precision.

Unfortunately JavaScript struggles with integer numbers bigger than
`Number.MAX_SAFE_INTEGER`. This is due to JavaScript's implementation of
integer values as `double`. For anything bigger than this max safe
integer value JavaScript provides a `BigInt` type.

The first issue with this behavior is that when parsing JSON, there is
no way of telling which "number flavor" to expect, APIs just don't allow
us to define so. Another issue is that the default JSON
serializer/deserializer doesn't even know how to handle `BigInt`.

This commit fixes those issues as followed:

1. Use a custom JSON parser `json-bigint` that can serialize and
   deserialze `BigInt` values. A quirk is that it will only deserialize
   a number into a `BigInt` if the serialized value ends up being bigger
   than JavaScript's max safe integer. This means there is ambiguity
   when deserialing data.
2. To address the ambiguity issue, I added a TypeScript mapped type
   `BigintOrNumber<T>` that given a type `T` will replace all fields
   that are either `bigint` or `number`. Note that this is only static
   typing to teach TypeScript about this behavior. Then when receiving
   messages, one has to define `Normalizer` functions which will take
   this ambiguous type as input and return a "normalized" version that
   matches the original type `T`. See this as post-processing to make
   sure the received data is using the proper data types in the right
   places.
3. Rewrite all the tests to validate this logic using test data as
   coming out of the current Trace Server.

Signed-off-by: Paul Marechal <paul.marechal@ericsson.com>
Co-authored-by: Patrick Tasse <patrick.tasse@ericsson.com>
  • Loading branch information
paul-marechal and PatrickTasse committed Oct 1, 2021
1 parent d3f4556 commit f2b5b51
Show file tree
Hide file tree
Showing 46 changed files with 1,265 additions and 361 deletions.
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
indent_size = 4
indent_style = space

[*.test.ts]
indent_size = 2
indent_style = space

[*.json]
indent_size = 2
indent_style = space
2 changes: 1 addition & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
"json",
"node"
]
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"typescript": "latest"
},
"dependencies": {
"@types/json-bigint": "^1.0.1",
"json-bigint": "^1.0.0",
"node-fetch": "^2.5.0"
},
"scripts": {
Expand Down
23 changes: 21 additions & 2 deletions src/models/annotation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { assertNumber, Normalizer } from '../protocol/serialization';
import { OutputElementStyle } from './styles';

export enum Type {
Expand All @@ -9,10 +10,28 @@ export interface AnnotationCategoriesModel {
annotationCategories: string[];
}

export const AnnotationModel: Normalizer<AnnotationModel> = input => {
const annotations: AnnotationModel['annotations'] = {};
for (const [category, annotationArray] of Object.entries(input.annotations)) {
annotations[category] = annotationArray.map(Annotation);
}
return { annotations };
};

export interface AnnotationModel {
annotations: { [category: string]: Annotation[] };
}

export const Annotation: Normalizer<Annotation> = input => {
const { duration, entryId, time, ...rest } = input;
return {
...rest,
duration: BigInt(duration),
entryId: assertNumber(entryId),
time: BigInt(time),
};
};

/**
* Model for annotation
*/
Expand All @@ -26,12 +45,12 @@ export interface Annotation {
/**
* Time of the annotation
*/
time: number;
time: bigint;

/**
* Duration of the annotation
*/
duration: number;
duration: bigint;

/**
* Entry Id of the annotation
Expand Down
15 changes: 13 additions & 2 deletions src/models/bookmark.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import { Normalizer } from '../protocol/serialization';

export const Bookmark: Normalizer<Bookmark> = input => {
const { endTime, startTime, ...rest } = input;
return {
...rest,
endTime: BigInt(endTime),
startTime: BigInt(startTime),
};
};

/**
* Model for bookmark
*/
Expand All @@ -15,12 +26,12 @@ export interface Bookmark {
/**
* Start time for the bookmark
*/
startTime: number;
startTime: bigint;

/**
* End time for the bookmark
*/
endTime: number;
endTime: bigint;

/**
* Type of the bookmark
Expand Down
23 changes: 23 additions & 0 deletions src/models/entry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { assertNumber, Normalizer } from '../protocol/serialization';
import { OutputElementStyle } from './styles';

export const Entry: Normalizer<Entry> = input => {
const { id, parentId, ...rest } = input;
const entry: Entry = {
...rest,
id: assertNumber(id),
};
if (parentId !== undefined) {
entry.parentId = assertNumber(parentId);
}
return entry;
};

/**
* Basic entry interface
*/
Expand Down Expand Up @@ -45,6 +58,16 @@ export interface EntryHeader {
tooltip: string
}

export function EntryModel<T extends Entry>(normalizer: Normalizer<T>): Normalizer<EntryModel<T>> {
return input => {
let { entries, ...rest } = input;
return {
...rest,
entries: entries.map(normalizer),
};
};
}

/**
* Entry model that will be returned by the server
*/
Expand Down
16 changes: 14 additions & 2 deletions src/models/experiment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { assertNumber, Normalizer } from '../protocol/serialization';
import { Trace } from './trace';

export const Experiment: Normalizer<Experiment> = input => {
const { end, nbEvents, start, traces, ...rest } = input;
return {
...rest,
end: BigInt(end),
nbEvents: assertNumber(nbEvents),
start: BigInt(start),
traces: traces.map(Trace),
};
};

/**
* Model of an experiment that contain one or more traces
*/
Expand All @@ -17,12 +29,12 @@ export interface Experiment {
/**
* Experiment's start time
*/
start: number;
start: bigint;

/**
* Experiment's end time
*/
end: number;
end: bigint;

/**
* Current number of events
Expand Down
4 changes: 2 additions & 2 deletions src/models/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export interface Filter {
/**
* Start time of the filter
*/
startTime: number;
startTime: bigint;

/**
* End time of the filter
*/
endTime: number;
endTime: bigint;

/**
* Expression from the filtering language
Expand Down
24 changes: 19 additions & 5 deletions src/models/output-descriptor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { Normalizer } from '../protocol/serialization';

export const OutputDescriptor: Normalizer<OutputDescriptor> = input => {
const { end, start, ...rest } = input;
const outputDescriptor: OutputDescriptor = rest;
if (end !== undefined) {
outputDescriptor.end = BigInt(end);
}
if (start !== undefined) {
outputDescriptor.start = BigInt(start);
}
return outputDescriptor;
};

/**
* Descriptor of a specific output provider
*/
Expand Down Expand Up @@ -26,26 +40,26 @@ export interface OutputDescriptor {
/**
* Map of query parameters that the provider accept
*/
queryParameters: Map<string, any>;
queryParameters?: Record<string, any>;

/**
* Start time
*/
start: number;
start?: bigint;

/**
* End time
*/
end: number;
end?: bigint;

/**
* Indicate if the start, end times and current model are final,
* or if they will need to be refreshed later to represent a more up to date version
*/
final: boolean;
final?: boolean;

/**
* List of compatible outputs that can be used in the same view (ex. as overlay)
*/
compatibleProviders: string[];
compatibleProviders?: string[];
}
10 changes: 5 additions & 5 deletions src/models/query/query-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ describe('Query helper tests', () => {
});

it('Should build a simple time query', () => {
const array = [1, 2, 3];
const array = [BigInt(1), BigInt(2), BigInt(3)];
const query = new Query({ [QueryHelper.REQUESTED_TIMES_KEY]: array });
const test = QueryHelper.timeQuery(array);

expect(test).toEqual(query);
});

it('Should build a simple time query with selected items', () => {
const times = [1, 2, 3];
const times = [BigInt(1), BigInt(2), BigInt(3)];
const items = [4, 5, 6];
const query = new Query({
[QueryHelper.REQUESTED_TIMES_KEY]: times,
Expand All @@ -47,10 +47,10 @@ describe('Query helper tests', () => {
});

it('Should split the range into equal parts', () => {
const start = 10;
const end = 20;
const start = BigInt(10);
const end = BigInt(20);
const parts = 3;
const array = [10, 15, 20];
const array = [BigInt(10), BigInt(15), BigInt(20)];
const test = QueryHelper.splitRangeIntoEqualParts(start, end, parts);

expect(test).toEqual(array);
Expand Down
39 changes: 22 additions & 17 deletions src/models/query/query-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,25 @@ export class QueryHelper {

/**
* Build a simple time query
* @param timeRequested Array of requested times
* @param requestedTimes Array of requested times
* @param additionalProperties Use this optional parameter to add custom properties to your query
*/
public static timeQuery(timeRequested: number[], additionalProperties?: { [key: string]: any }): Query {
public static timeQuery(requestedTimes: bigint[], additionalProperties?: { [key: string]: any }): Query {
const timeObj = {
[this.REQUESTED_TIMES_KEY]: timeRequested
[this.REQUESTED_TIMES_KEY]: requestedTimes
};
return new Query({ ...timeObj, ...additionalProperties });
}

/**
* Build a simple time query with selected items
* @param timeRequested Array of requested times
* @param requestedTimes Array of requested times
* @param items Array of item IDs
* @param additionalProperties Use this optional parameter to add custom properties to your query
*/
public static selectionTimeQuery(timeRequested: number[], items: number[], additionalProperties?: { [key: string]: any }): Query {
public static selectionTimeQuery(requestedTimes: bigint[], items: number[], additionalProperties?: { [key: string]: any }): Query {
const selectionTimeObj = {
[this.REQUESTED_TIMES_KEY]: timeRequested,
[this.REQUESTED_TIMES_KEY]: requestedTimes,
[this.REQUESTED_ITEMS_KEY]: items
};

Expand Down Expand Up @@ -90,22 +90,27 @@ export class QueryHelper {
* Split the range into equal parts
* @param start Start time
* @param end End time
* @param nb Number of element or resolution
* @param nb Number of elements
*/
public static splitRangeIntoEqualParts(start: number, end: number, nb: number): number[] {
const result: number[] = new Array(nb);
public static splitRangeIntoEqualParts(start: bigint, end: bigint, nb: number): bigint[] {
if (nb <= 0) {
return [];
}
if (nb === 1) {
if (start === end) {
result[0] = start;
return result;
}
return [start];
}

const stepSize: number = Math.abs(end - start) / (nb - 1);
if (start > end) {
const tmp = end;
end = start;
start = tmp;
}

const result: bigint[] = new Array(nb);
const stepSize: number = Number(end - start) / (nb - 1);
for (let i = 0; i < nb; i++) {
result[i] = Math.min(start, end) + Math.round(i * stepSize);
result[i] = start + BigInt(Math.floor(i * stepSize));
}
result[result.length - 1] = Math.max(start, end);
result[result.length - 1] = end;
return result;
}
}
2 changes: 2 additions & 0 deletions src/models/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
* The output response will contain only elements that pass these filters.
*/
export class Query {

/**
* Map of parameters used for the query
*/
// @ts-expect-error TS doesn't like unused private fields.
private parameters: object;

/**
Expand Down

0 comments on commit f2b5b51

Please sign in to comment.