From e73a125b92a835c3398492615975892123a3ac4e Mon Sep 17 00:00:00 2001 From: Lucas Leblow Date: Fri, 16 Aug 2024 12:43:26 -0600 Subject: [PATCH] crdx: Improve performance of calculateConcurrency --- packages/crdx/src/graph/concurrency.ts | 44 ++++++++++------ packages/crdx/src/graph/getSuccessors.ts | 22 -------- packages/crdx/src/graph/index.ts | 2 - packages/crdx/src/graph/isSuccessor.ts | 21 -------- .../crdx/src/graph/test/getSuccessors.test.ts | 50 ------------------- 5 files changed, 30 insertions(+), 109 deletions(-) delete mode 100644 packages/crdx/src/graph/getSuccessors.ts delete mode 100644 packages/crdx/src/graph/isSuccessor.ts delete mode 100644 packages/crdx/src/graph/test/getSuccessors.test.ts diff --git a/packages/crdx/src/graph/concurrency.ts b/packages/crdx/src/graph/concurrency.ts index c0dc61f7..47597f4b 100644 --- a/packages/crdx/src/graph/concurrency.ts +++ b/packages/crdx/src/graph/concurrency.ts @@ -1,9 +1,8 @@ import { memoize } from '@localfirst/shared' import { type Hash } from 'util/index.js' -import { getHashes } from './getHashes.js' import { getLink } from './getLink.js' -import { isPredecessorHash } from './isPredecessor.js' -import { isSuccessorHash } from './isSuccessor.js' +import { getPredecessorHashes } from './getPredecessors.js' +import { getChildrenHashes } from './children.js' import type { Action, Graph, Link } from './types.js' /** Returns all links that are concurrent with the given link. */ @@ -30,24 +29,41 @@ export const getConcurrentHashes = (graph: Graph, hash: Hash): Hash[] * ``` */ export const calculateConcurrency = memoize((graph: Graph) => { + const toVisit = [graph.root] + const visited: Set = new Set() const concurrencyLookup: Record = {} - // for each link, find all links that are concurrent with it - for (const _ in graph.links) { - const hash = _ as Hash - concurrencyLookup[hash] = getHashes(graph) - .filter(b => isConcurrent(graph, hash, b)) - .sort() + while (toVisit.length > 0) { + const current = toVisit.shift() as Hash + + if (visited.has(current)) { + continue + } + + const predecessors = new Set(getPredecessorHashes(graph, current)) + const concurrent: Hash[] = [] + + for (const v of Array.from(visited)) { + if (!predecessors.has(v) && !getPredecessorHashes(graph, v).includes(current)) { + concurrent.push(v) + } + } + + concurrencyLookup[current] = concurrent + + for (const c of concurrent) { + concurrencyLookup[c].push(current) + } + + for (const c of getChildrenHashes(graph, current)) { + toVisit.push(c) + } + visited.add(current) } return concurrencyLookup }) -export const isConcurrent = (graph: Graph, a: Hash, b: Hash) => - a !== b && // a link isn't concurrent with itself - !isPredecessorHash(graph, a, b) && // a link isn't concurrent with any of its predecessors - !isSuccessorHash(graph, a, b) // a link isn't concurrent with any of its successors - export const getConcurrentBubbles = (graph: Graph): Hash[][] => { const seen: Record = {} diff --git a/packages/crdx/src/graph/getSuccessors.ts b/packages/crdx/src/graph/getSuccessors.ts deleted file mode 100644 index dc4ecdc3..00000000 --- a/packages/crdx/src/graph/getSuccessors.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { memoize } from '@localfirst/shared' -import { uniq } from 'lodash-es' -import { type Hash } from 'util/index.js' -import { getChildrenHashes } from './children.js' -import type { Action, Graph, Link } from './types.js' - -const memoizeResolver = (graph: Graph, hash: Hash) => `${graph.head.join('')}:${hash}` - -export const getSuccessorHashes = memoize((graph: Graph, hash: Hash): Hash[] => { - const children = getChildrenHashes(graph, hash) - const successors = children.flatMap(parent => getSuccessorHashes(graph, parent)) - return uniq(children.concat(successors)) -}, memoizeResolver) - -/** Returns the set of successors of `link` (not including `link`) */ -export const getSuccessors = ( - graph: Graph, - link: Link -): Array> => - getSuccessorHashes(graph, link.hash) - .map(h => graph.links[h]) - .filter(link => link !== undefined) diff --git a/packages/crdx/src/graph/index.ts b/packages/crdx/src/graph/index.ts index 705f8ab5..bab5acb2 100644 --- a/packages/crdx/src/graph/index.ts +++ b/packages/crdx/src/graph/index.ts @@ -12,10 +12,8 @@ export * from './getParents.js' export * from './getPredecessors.js' export * from './getRoot.js' export * from './getSequence.js' -export * from './getSuccessors.js' export * from './headsAreEqual.js' export * from './isPredecessor.js' -export * from './isSuccessor.js' export * from './merge.js' export * from './redactGraph.js' export * from './serialize.js' diff --git a/packages/crdx/src/graph/isSuccessor.ts b/packages/crdx/src/graph/isSuccessor.ts deleted file mode 100644 index 7f5eb924..00000000 --- a/packages/crdx/src/graph/isSuccessor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type Action, type Link, type Graph } from './types.js' -import { type Hash } from 'util/index.js' -import { getSuccessorHashes } from './getSuccessors.js' - -/** Returns true if `a` is a successor of `b` */ -export const isSuccessor = ( - graph: Graph, - a: Link, - b: Link -): boolean => { - return ( - a !== undefined && - b !== undefined && - a.hash in graph.links && - b.hash in graph.links && - getSuccessorHashes(graph, b.hash).includes(a.hash) - ) -} - -export const isSuccessorHash = (graph: Graph, a: Hash, b: Hash) => - getSuccessorHashes(graph, b).includes(a) diff --git a/packages/crdx/src/graph/test/getSuccessors.test.ts b/packages/crdx/src/graph/test/getSuccessors.test.ts deleted file mode 100644 index 8e10b8b2..00000000 --- a/packages/crdx/src/graph/test/getSuccessors.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it, test } from 'vitest' -import { type XLink, buildGraph, findByPayload, getPayloads } from 'util/testing/graph.js' -import { getRoot, getSuccessors, isSuccessor } from 'graph/index.js' - -describe('graphs', () => { - describe('successors', () => { - const graph = buildGraph(` - ┌─ e ─ g ─┐ - ┌─ c ─ d ─┤ ├─ o ─┐ - a ─ b ─┤ └─── f ───┤ ├─ n - ├──── h ──── i ─────┘ │ - └───── j ─── k ── l ──────┘ - `) - - describe('getSuccessors', () => { - const getSuccessorPayloads = (link: XLink): string => { - const successors = getSuccessors(graph, link) - return getPayloads(successors).split('').sort().join('') - } - - test('root', () => { - const root = getRoot(graph) - expect(getSuccessorPayloads(root)).toEqual('abcdefghijklno') - }) - - test('d', () => { - const d = findByPayload(graph, 'd') - expect(getSuccessorPayloads(d)).toEqual('efgno') - }) - - test('o', () => { - const o = findByPayload(graph, 'o') - expect(getSuccessorPayloads(o)).toEqual('n') - }) - }) - - describe('isSuccessor', () => { - const testCase = (a: string, b: string) => { - const aLink = findByPayload(graph, a) - const bLink = findByPayload(graph, b) - - return isSuccessor(graph, aLink, bLink) - } - - it('f succeeds c', () => expect(testCase('f', 'c')).toBe(true)) - it(`c doesn't succeed f`, () => expect(testCase('c', 'f')).toBe(false)) - it(`c doesn't succeed c`, () => expect(testCase('c', 'c')).toBe(false)) - }) - }) -})