Skip to content

Commit 5fb352a

Browse files
Use approximate FieldValue size for MemoryLRU (#2548)
1 parent 5042f72 commit 5fb352a

File tree

6 files changed

+177
-26
lines changed

6 files changed

+177
-26
lines changed

packages/firestore/src/api/blob.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ export class Blob {
120120
return this._binaryString === other._binaryString;
121121
}
122122

123+
_approximateByteSize(): number {
124+
// Assume UTF-16 encoding in memory (see StringValue.approximateByteSize())
125+
return this._binaryString.length * 2;
126+
}
127+
123128
/**
124129
* Actually private to JS consumers of our API, so this function is prefixed
125130
* with an underscore.

packages/firestore/src/local/memory_persistence.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616
*/
1717

1818
import { User } from '../auth/user';
19-
import { MaybeDocument } from '../model/document';
19+
import { Document, MaybeDocument } from '../model/document';
2020
import { DocumentKey } from '../model/document_key';
21-
import { JsonProtoSerializer } from '../remote/serializer';
2221
import { fail } from '../util/assert';
2322
import { debug } from '../util/log';
2423
import * as obj from '../util/obj';
2524
import { ObjectMap } from '../util/obj_map';
2625
import { encode } from './encoded_resource_path';
27-
import { LocalSerializer } from './local_serializer';
2826
import {
2927
ActiveTargets,
3028
LruDelegate,
@@ -77,11 +75,10 @@ export class MemoryPersistence implements Persistence {
7775

7876
static createLruPersistence(
7977
clientId: ClientId,
80-
serializer: JsonProtoSerializer,
8178
params: LruParams
8279
): MemoryPersistence {
8380
const factory = (p: MemoryPersistence): MemoryLruDelegate =>
84-
new MemoryLruDelegate(p, new LocalSerializer(serializer), params);
81+
new MemoryLruDelegate(p, params);
8582
return new MemoryPersistence(clientId, factory);
8683
}
8784

@@ -334,7 +331,6 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate {
334331

335332
constructor(
336333
private readonly persistence: MemoryPersistence,
337-
private readonly serializer: LocalSerializer,
338334
lruParams: LruParams
339335
) {
340336
this.garbageCollector = new LruGarbageCollector(this, lruParams);
@@ -471,21 +467,11 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate {
471467
}
472468

473469
documentSize(maybeDoc: MaybeDocument): number {
474-
const remoteDocument = this.serializer.toDbRemoteDocument(
475-
maybeDoc,
476-
maybeDoc.version
477-
);
478-
let value: unknown;
479-
if (remoteDocument.document) {
480-
value = remoteDocument.document;
481-
} else if (remoteDocument.unknownDocument) {
482-
value = remoteDocument.unknownDocument;
483-
} else if (remoteDocument.noDocument) {
484-
value = remoteDocument.noDocument;
485-
} else {
486-
throw fail('Unknown remote document type');
470+
let documentSize = maybeDoc.key.toString().length;
471+
if (maybeDoc instanceof Document) {
472+
documentSize += maybeDoc.data().approximateByteSize();
487473
}
488-
return JSON.stringify(value).length;
474+
return documentSize;
489475
}
490476

491477
private isPinned(

packages/firestore/src/model/field_value.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ export abstract class FieldValue {
126126
abstract isEqual(other: FieldValue): boolean;
127127
abstract compareTo(other: FieldValue): number;
128128

129+
/**
130+
* Returns an approximate (and wildly inaccurate) in-memory size for the field
131+
* value.
132+
*
133+
* The memory size takes into account only the actual user data as it resides
134+
* in memory and ignores object overhead.
135+
*/
136+
abstract approximateByteSize(): number;
137+
129138
toString(): string {
130139
const val = this.value();
131140
return val === null ? 'null' : val.toString();
@@ -167,6 +176,10 @@ export class NullValue extends FieldValue {
167176
return this.defaultCompareTo(other);
168177
}
169178

179+
approximateByteSize(): number {
180+
return 4;
181+
}
182+
170183
static INSTANCE = new NullValue();
171184
}
172185

@@ -195,6 +208,10 @@ export class BooleanValue extends FieldValue {
195208
return this.defaultCompareTo(other);
196209
}
197210

211+
approximateByteSize(): number {
212+
return 4;
213+
}
214+
198215
static of(value: boolean): BooleanValue {
199216
return value ? BooleanValue.TRUE : BooleanValue.FALSE;
200217
}
@@ -221,6 +238,10 @@ export abstract class NumberValue extends FieldValue {
221238
}
222239
return this.defaultCompareTo(other);
223240
}
241+
242+
approximateByteSize(): number {
243+
return 8;
244+
}
224245
}
225246

226247
/** Utility function to compare doubles (using Firestore semantics for NaN). */
@@ -313,6 +334,13 @@ export class StringValue extends FieldValue {
313334
}
314335
return this.defaultCompareTo(other);
315336
}
337+
338+
approximateByteSize(): number {
339+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures:
340+
// "JavaScript's String type is [...] a set of elements of 16-bit unsigned
341+
// integer values"
342+
return this.internalValue.length * 2;
343+
}
316344
}
317345

318346
export class TimestampValue extends FieldValue {
@@ -347,6 +375,11 @@ export class TimestampValue extends FieldValue {
347375
return this.defaultCompareTo(other);
348376
}
349377
}
378+
379+
approximateByteSize(): number {
380+
// Timestamps are made up of two distinct numbers (seconds + nanoseconds)
381+
return 16;
382+
}
350383
}
351384

352385
/**
@@ -410,6 +443,13 @@ export class ServerTimestampValue extends FieldValue {
410443
toString(): string {
411444
return '<ServerTimestamp localTime=' + this.localWriteTime.toString() + '>';
412445
}
446+
447+
approximateByteSize(): number {
448+
return (
449+
/* localWriteTime */ 16 +
450+
(this.previousValue ? this.previousValue.approximateByteSize() : 0)
451+
);
452+
}
413453
}
414454

415455
export class BlobValue extends FieldValue {
@@ -436,6 +476,10 @@ export class BlobValue extends FieldValue {
436476
}
437477
return this.defaultCompareTo(other);
438478
}
479+
480+
approximateByteSize(): number {
481+
return this.internalValue._approximateByteSize();
482+
}
439483
}
440484

441485
export class RefValue extends FieldValue {
@@ -466,6 +510,14 @@ export class RefValue extends FieldValue {
466510
}
467511
return this.defaultCompareTo(other);
468512
}
513+
514+
approximateByteSize(): number {
515+
return (
516+
this.databaseId.projectId.length +
517+
this.databaseId.database.length +
518+
this.key.toString().length
519+
);
520+
}
469521
}
470522

471523
export class GeoPointValue extends FieldValue {
@@ -492,6 +544,11 @@ export class GeoPointValue extends FieldValue {
492544
}
493545
return this.defaultCompareTo(other);
494546
}
547+
548+
approximateByteSize(): number {
549+
// GeoPoints are made up of two distinct numbers (latitude + longitude)
550+
return 16;
551+
}
495552
}
496553

497554
export class ObjectValue extends FieldValue {
@@ -634,6 +691,14 @@ export class ObjectValue extends FieldValue {
634691
return FieldMask.fromSet(fields);
635692
}
636693

694+
approximateByteSize(): number {
695+
let size = 0;
696+
this.internalValue.inorderTraversal((key, val) => {
697+
size += key.length + val.approximateByteSize();
698+
});
699+
return size;
700+
}
701+
637702
toString(): string {
638703
return this.internalValue.toString();
639704
}
@@ -720,6 +785,13 @@ export class ArrayValue extends FieldValue {
720785
}
721786
}
722787

788+
approximateByteSize(): number {
789+
return this.internalValue.reduce(
790+
(totalSize, value) => totalSize + value.approximateByteSize(),
791+
0
792+
);
793+
}
794+
723795
toString(): string {
724796
const descriptions = this.internalValue.map(v => v.toString());
725797
return `[${descriptions.join(',')}]`;

packages/firestore/test/unit/local/persistence_test_helpers.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,7 @@ export async function testMemoryEagerPersistence(): Promise<MemoryPersistence> {
128128
export async function testMemoryLruPersistence(
129129
params: LruParams = LruParams.DEFAULT
130130
): Promise<MemoryPersistence> {
131-
return MemoryPersistence.createLruPersistence(
132-
AutoId.newId(),
133-
JSON_SERIALIZER,
134-
params
135-
);
131+
return MemoryPersistence.createLruPersistence(AutoId.newId(), params);
136132
}
137133

138134
/** Clears the persistence in tests */

packages/firestore/test/unit/model/field_value.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { expect } from 'chai';
1919
import { GeoPoint } from '../../../src/api/geo_point';
2020
import { Timestamp } from '../../../src/api/timestamp';
2121
import * as fieldValue from '../../../src/model/field_value';
22+
import { primitiveComparator } from '../../../src/util/misc';
2223
import * as typeUtils from '../../../src/util/types';
2324
import {
2425
blob,
@@ -485,4 +486,96 @@ describe('FieldValue', () => {
485486
}
486487
);
487488
});
489+
490+
it('estimates size correctly for fixed sized values', () => {
491+
// This test verifies that each member of a group takes up the same amount
492+
// of space in memory (based on its estimated in-memory size).
493+
const equalityGroups = [
494+
{ expectedByteSize: 4, elements: [wrap(null), wrap(false), wrap(true)] },
495+
{
496+
expectedByteSize: 4,
497+
elements: [wrap(blob(0, 1)), wrap(blob(128, 129))]
498+
},
499+
{
500+
expectedByteSize: 8,
501+
elements: [wrap(NaN), wrap(Infinity), wrap(1), wrap(1.1)]
502+
},
503+
{
504+
expectedByteSize: 16,
505+
elements: [wrap(new GeoPoint(0, 0)), wrap(new GeoPoint(0, 0))]
506+
},
507+
{
508+
expectedByteSize: 16,
509+
elements: [wrap(Timestamp.fromMillis(100)), wrap(Timestamp.now())]
510+
},
511+
{
512+
expectedByteSize: 16,
513+
elements: [
514+
new fieldValue.ServerTimestampValue(Timestamp.fromMillis(100), null),
515+
new fieldValue.ServerTimestampValue(Timestamp.now(), null)
516+
]
517+
},
518+
{
519+
expectedByteSize: 20,
520+
elements: [
521+
new fieldValue.ServerTimestampValue(
522+
Timestamp.fromMillis(100),
523+
wrap(true)
524+
),
525+
new fieldValue.ServerTimestampValue(Timestamp.now(), wrap(false))
526+
]
527+
},
528+
{
529+
expectedByteSize: 11,
530+
elements: [
531+
new fieldValue.RefValue(dbId('p1', 'd1'), key('c1/doc1')),
532+
new fieldValue.RefValue(dbId('p2', 'd2'), key('c2/doc2'))
533+
]
534+
},
535+
{ expectedByteSize: 6, elements: [wrap('foo'), wrap('bar')] },
536+
{ expectedByteSize: 4, elements: [wrap(['a', 'b']), wrap(['c', 'd'])] },
537+
{
538+
expectedByteSize: 6,
539+
elements: [wrap({ a: 'a', b: 'b' }), wrap({ c: 'c', d: 'd' })]
540+
}
541+
];
542+
543+
for (const group of equalityGroups) {
544+
for (const element of group.elements) {
545+
expect(element.approximateByteSize()).to.equal(group.expectedByteSize);
546+
}
547+
}
548+
});
549+
550+
it('estimates size correctly for relatively sized values', () => {
551+
// This test verifies for each group that the estimated size increases
552+
// as the size of the underlying data grows.
553+
const relativeGroups = [
554+
[wrap(blob(0)), wrap(blob(0, 1))],
555+
[
556+
new fieldValue.ServerTimestampValue(Timestamp.fromMillis(100), null),
557+
new fieldValue.ServerTimestampValue(Timestamp.now(), wrap(null))
558+
],
559+
[
560+
new fieldValue.RefValue(dbId('p1', 'd1'), key('c1/doc1')),
561+
new fieldValue.RefValue(dbId('p1', 'd1'), key('c1/doc1/c2/doc2'))
562+
],
563+
[wrap('foo'), wrap('foobar')],
564+
[wrap(['a', 'b']), wrap(['a', 'bc'])],
565+
[wrap(['a', 'b']), wrap(['a', 'b', 'c'])],
566+
[wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', b: 'bc' })],
567+
[wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', bc: 'b' })],
568+
[wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', b: 'b', c: 'c' })]
569+
];
570+
571+
for (const group of relativeGroups) {
572+
const expectedOrder = group;
573+
const actualOrder = group
574+
.slice()
575+
.sort((l, r) =>
576+
primitiveComparator(l.approximateByteSize(), r.approximateByteSize())
577+
);
578+
expect(expectedOrder).to.deep.equal(actualOrder);
579+
}
580+
});
488581
});

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,6 @@ class MemoryTestRunner extends TestRunner {
12381238
? MemoryPersistence.createEagerPersistence(this.clientId)
12391239
: MemoryPersistence.createLruPersistence(
12401240
this.clientId,
1241-
serializer,
12421241
LruParams.DEFAULT
12431242
)
12441243
);

0 commit comments

Comments
 (0)