Skip to content

Commit

Permalink
(split from Whiteboard) Merged PR 32084: perf: Optimize getStartingPoint
Browse files Browse the repository at this point in the history
This PR aims to optimize `getStartingPoint(Number.POSITIVE_INFINITY)` when no local changes exist by caching the edit with the highest revision in CachingLogViewer.

See approach \#3 from #59391

**Before Optimization Perf Results**:
```
status  name                                         period (ns/op)  root mean error  iterations  samples  total time (s)
------  -------------------------------------------  --------------  ---------------  ----------  -------  --------------
    ✔   get currentView with 1 sequenced edit(s)              256.3           ±5.32%  19,175,112       71            5.59
    ✔   get currentView with 1000 sequenced edit(s)           678.1           ±4.80%   7,157,358       73            5.43
```

**After Optimization Perf Results**:
```
status  name                                       period (ns/op)  root mean error  iterations  samples  total time (s)
------  -----------------------------------------  --------------  ---------------  ----------  -------  --------------
    ✔   get currentView with 1 sequenced edits              151.4           ±4.39%  32,649,075       81            5.57
    ✔   get currentView with 1000 sequenced edits           178.3           ±4.38%  27,418,925       79            5.50
```

Related work items: #59651
  • Loading branch information
yoonk-msft committed Jul 21, 2021
1 parent a1f9898 commit bed2bad
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/LogViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ export class CachingLogViewer<TChange> implements LogViewer {

private readonly transactionFactory: (revisionView: RevisionView) => GenericTransaction<TChange>;

/**
* Cache entry for the highest revision.
* `undefined` when not cached.
*/
private highestRevisionCacheEntry?: EditCacheEntry<TChange>;

/**
* @returns true if the highest revision is cached.
*/
public highestRevisionCached(): boolean {
return this.highestRevisionCacheEntry !== undefined;
}

/**
* Create a new LogViewer
* @param log - the edit log which revisions will be based on.
Expand Down Expand Up @@ -247,6 +260,10 @@ export class CachingLogViewer<TChange> implements LogViewer {
* being interleaved with remote edits.
*/
private handleEditAdded(edit: Edit<TChange>, isLocal: boolean, wasLocal: boolean): void {
// Clear highestRevisionCacheEntry, since what revision is highest might change.
// Note that as an optimization we could skip clearing this when a local edit is sequenced.
this.highestRevisionCacheEntry = undefined;

if (isLocal) {
this.unappliedSelfEdits.push(edit.id);
} else if (wasLocal) {
Expand Down Expand Up @@ -335,6 +352,12 @@ export class CachingLogViewer<TChange> implements LogViewer {
private getStartingPoint(revision: Revision): { startRevision: Revision } & EditCacheEntry<TChange> {
// Per the documentation for revision, the returned view should be the output of the edit at the largest index <= `revision`.
const revisionClamped = Math.min(revision, this.log.length);

// If the highest revision is requested, and it's cached, use highestRevisionCacheEntry.
if (revisionClamped === this.log.length && this.highestRevisionCacheEntry !== undefined) {
return { ...this.highestRevisionCacheEntry, startRevision: revisionClamped };
}

let current: EditCacheEntry<TChange>;
let startRevision: Revision;
const { numberOfSequencedEdits } = this.log;
Expand Down Expand Up @@ -421,6 +444,11 @@ export class CachingLogViewer<TChange> implements LogViewer {
this.localRevisionCache.push(computedCacheEntry);
}

// Only update highestRevisionCacheEntry if this snapshot is the highest revision.
if (revision >= this.log.length) {
this.highestRevisionCacheEntry = computedCacheEntry;
}

this.processEditStatus(editingResult.status, this.log.getIdAtIndex(editIndex), cached);
return computedCacheEntry;
}
Expand Down
15 changes: 15 additions & 0 deletions src/test/LogViewer.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,21 @@ describe('CachingLogViewer', () => {
expect(viewer.getEditResultInSession(3).status).equals(EditStatus.Invalid);
});

it('caches the highest revision', async () => {
const log = getSimpleLog();
const viewer = getCachingLogViewerAssumeAppliedEdits(log, simpleRevisionViewNoTraits);
expect(viewer.highestRevisionCached()).to.be.false;
await requestAllRevisionViews(viewer, log);
expect(viewer.highestRevisionCached()).to.be.true;
log.addLocalEdit(newEdit(Insert.create([makeEmptyNode()], StablePlace.atEndOf(rightTraitLocation))));
log.addSequencedEdit(newEdit(Insert.create([makeEmptyNode()], StablePlace.atEndOf(rightTraitLocation))), {
sequenceNumber: 3,
referenceSequenceNumber: 2,
minimumSequenceNumber: 2,
});
expect(viewer.highestRevisionCached()).to.be.false;
});

it('evicts least recently set cached revision views for sequenced edits', async () => {
let editsProcessed = 0;
const log = getSimpleLog(CachingLogViewer.sequencedCacheSizeMax * 2);
Expand Down
43 changes: 43 additions & 0 deletions src/test/SharedTree.perf.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { benchmark, BenchmarkType } from '@fluid-tools/benchmark';
import { MockContainerRuntimeFactory } from '@fluidframework/test-runtime-utils';
import { assert } from '../Common';
import { Change, SharedTree } from '../default-edits';
import { EditLog } from '../EditLog';
import { createStableEdits, setUpTestSharedTree, simpleTestTree } from './utilities/TestUtilities';

describe('SharedTree Perf', () => {
let tree: SharedTree | undefined;
let containerRuntimeFactory: MockContainerRuntimeFactory | undefined;
for (const count of [1, 1_000]) {
benchmark({
type: BenchmarkType.Measurement,
title: `get currentView with ${count} sequenced edit(s)`,
before: () => {
({ tree, containerRuntimeFactory } = setUpTestSharedTree({ initialTree: simpleTestTree }));

const edits = createStableEdits(count);
for (let i = 0; i < count - 1; i++) {
tree.processLocalEdit(edits[i]);
}

containerRuntimeFactory.processAllMessages();
const editLog = tree.edits as EditLog<Change>;
assert(editLog.numberOfSequencedEdits === count);
assert(editLog.numberOfLocalEdits === 0);
},
benchmarkFn: () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tree!.currentView;
},
after: () => {
tree = undefined;
containerRuntimeFactory = undefined;
},
});
}
});

0 comments on commit bed2bad

Please sign in to comment.