Skip to content

Explicit byte size accounting #2689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
58 changes: 57 additions & 1 deletion packages/firestore/src/model/proto_values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as api from '../protos/firestore_proto_api';

import { TypeOrder } from './field_value';
import { assert, fail } from '../util/assert';
import { keys, size } from '../util/obj';
import { forEach, keys, size } from '../util/obj';
import { ByteString } from '../util/byte_string';
import {
numericComparator,
Expand Down Expand Up @@ -417,6 +417,62 @@ function canonifyArray(arrayValue: api.ArrayValue): string {
return result + ']';
}

/**
* Returns an approximate (and wildly inaccurate) in-memory size for the field
* value.
*
* The memory size takes into account only the actual user data as it resides
* in memory and ignores object overhead.
*/
export function estimateByteSize(value: api.Value): number {
if ('nullValue' in value) {
return 4;
} else if ('booleanValue' in value) {
return 4;
} else if ('integerValue' in value) {
return 8;
} else if ('doubleValue' in value) {
return 8;
} else if ('timestampValue' in value) {
// TODO(mrschmidt: Add ServerTimestamp support
// Timestamps are made up of two distinct numbers (seconds + nanoseconds)
return 16;
} else if ('stringValue' in value) {
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures:
// "JavaScript's String type is [...] a set of elements of 16-bit unsigned
// integer values"
return value.stringValue!.length * 2;
} else if ('bytesValue' in value) {
return normalizeByteString(value.bytesValue!).approximateByteSize();
} else if ('referenceValue' in value) {
return value.referenceValue!.length;
} else if ('geoPointValue' in value) {
// GeoPoints are made up of two distinct numbers (latitude + longitude)
return 16;
} else if ('arrayValue' in value) {
return estimateArrayByteSize(value.arrayValue!);
} else if ('mapValue' in value) {
return estimateMapByteSize(value.mapValue!);
} else {
return fail('Invalid value type: ' + JSON.stringify(value));
}
}

function estimateMapByteSize(mapValue: api.MapValue): number {
let size = 0;
forEach(mapValue.fields || {}, (key, val) => {
size += key.length + estimateByteSize(val);
});
return size;
}

function estimateArrayByteSize(arrayValue: api.ArrayValue): number {
return (arrayValue.values || []).reduce(
(previousSize, value) => previousSize + estimateByteSize(value),
0
);
}

/**
* Converts the possible Proto values for a timestamp value into a "seconds and
* nanos" representation.
Expand Down
41 changes: 21 additions & 20 deletions packages/firestore/test/unit/model/field_value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
ObjectValue,
PrimitiveValue
} from '../../../src/model/proto_field_value';
import { canonicalId } from '../../../src/model/proto_values';
import { canonicalId, estimateByteSize } from '../../../src/model/proto_values';
import { ByteString } from '../../../src/util/byte_string';
import { primitiveComparator } from '../../../src/util/misc';
import * as typeUtils from '../../../src/util/types';
Expand Down Expand Up @@ -630,9 +630,7 @@ describe('FieldValue', () => {
);
});

// TODO(mrschmidt): Fix size accounting
// eslint-disable-next-line no-restricted-properties
it.skip('estimates size correctly for fixed sized values', () => {
it('estimates size correctly for fixed sized values', () => {
// This test verifies that each member of a group takes up the same amount
// of space in memory (based on its estimated in-memory size).
const equalityGroups = [
Expand All @@ -653,22 +651,23 @@ describe('FieldValue', () => {
expectedByteSize: 16,
elements: [wrap(Timestamp.fromMillis(100)), wrap(Timestamp.now())]
},
// TODO(mrschmidt): Support server timestamps
// {
// expectedByteSize: 16,
// elements: [
// new ServerTimestampValue(Timestamp.fromMillis(100), null),
// new ServerTimestampValue(Timestamp.now(), null)
// ]
// },
// {
// expectedByteSize: 20,
// elements: [
// new ServerTimestampValue(Timestamp.fromMillis(100), wrap(true)),
// new ServerTimestampValue(Timestamp.now(), wrap(false))
// ]
// },
{
expectedByteSize: 16,
elements: [
new ServerTimestampValue(Timestamp.fromMillis(100), null),
new ServerTimestampValue(Timestamp.now(), null)
]
},
{
expectedByteSize: 20,
elements: [
new ServerTimestampValue(Timestamp.fromMillis(100), wrap(true)),
new ServerTimestampValue(Timestamp.now(), wrap(false))
]
},
{
expectedByteSize: 11,
expectedByteSize: 42,
elements: [
wrapRef(dbId('p1', 'd1'), key('c1/doc1')),
wrapRef(dbId('p2', 'd2'), key('c2/doc2'))
Expand All @@ -684,7 +683,9 @@ describe('FieldValue', () => {

for (const group of equalityGroups) {
for (const element of group.elements) {
expect(element.approximateByteSize()).to.equal(group.expectedByteSize);
expect(estimateByteSize(element.proto)).to.equal(
group.expectedByteSize
);
}
}
});
Expand Down