diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index 91ddf582faa..fecdcfb487a 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -1,7 +1,12 @@ // @flow strict-local import type {GraphVisitor} from '@parcel/types'; -import type {ContentKey, NodeId, SerializedContentGraph} from '@parcel/graph'; +import type { + ContentGraphOpts, + ContentKey, + NodeId, + SerializedContentGraph, +} from '@parcel/graph'; import type { Asset, AssetGraphNode, @@ -31,6 +36,12 @@ type InitOpts = {| assetGroups?: Array, |}; +type AssetGraphOpts = {| + ...ContentGraphOpts, + symbolPropagationRan: boolean, + hash?: ?string, +|}; + type SerializedAssetGraph = {| ...SerializedContentGraph, hash?: ?string, @@ -104,7 +115,7 @@ export default class AssetGraph extends ContentGraph { envCache: Map; symbolPropagationRan: boolean; - constructor(opts: ?SerializedAssetGraph) { + constructor(opts: ?AssetGraphOpts) { if (opts) { let {hash, symbolPropagationRan, ...rest} = opts; super(rest); @@ -125,7 +136,7 @@ export default class AssetGraph extends ContentGraph { } // $FlowFixMe[prop-missing] - static deserialize(opts: SerializedAssetGraph): AssetGraph { + static deserialize(opts: AssetGraphOpts): AssetGraph { return new AssetGraph(opts); } diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index f9def5692ac..1c762f07be3 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -6,7 +6,11 @@ import type { Symbol, TraversalActions, } from '@parcel/types'; -import type {NodeId, SerializedContentGraph} from '@parcel/graph'; +import type { + ContentGraphOpts, + NodeId, + SerializedContentGraph, +} from '@parcel/graph'; import type { Asset, @@ -73,6 +77,14 @@ type InternalExportSymbolResolution = {| +exportAs: Symbol | string, |}; +type BundleGraphOpts = {| + graph: ContentGraphOpts, + bundleContentHashes: Map, + assetPublicIds: Set, + publicIdByAssetId: Map, + symbolPropagationRan: boolean, +|}; + type SerializedBundleGraph = {| $$raw: true, graph: SerializedContentGraph, @@ -174,7 +186,7 @@ export default class BundleGraph { let fromIds; if (assetGroupIds.has(edge.from)) { fromIds = [ - ...assetGraph.inboundEdges.getEdges( + ...assetGraph.getNodeIdsConnectedTo( edge.from, bundleGraphEdgeTypes.null, ), @@ -185,7 +197,7 @@ export default class BundleGraph { for (let from of fromIds) { if (assetGroupIds.has(edge.to)) { - for (let to of assetGraph.outboundEdges.getEdges( + for (let to of assetGraph.getNodeIdsConnectedFrom( edge.to, bundleGraphEdgeTypes.null, )) { @@ -223,7 +235,7 @@ export default class BundleGraph { }; } - static deserialize(serialized: SerializedBundleGraph): BundleGraph { + static deserialize(serialized: BundleGraphOpts): BundleGraph { return new BundleGraph({ graph: ContentGraph.deserialize(serialized.graph), assetPublicIds: serialized.assetPublicIds, @@ -1031,7 +1043,6 @@ export default class BundleGraph { }, visit, undefined, // start with root - // $FlowFixMe ALL_EDGE_TYPES, ); } @@ -1221,7 +1232,6 @@ export default class BundleGraph { return this._graph .getNodeIdsConnectedTo( this._graph.getNodeIdByContentKey(asset.id), - // $FlowFixMe ALL_EDGE_TYPES, ) .map(id => nullthrows(this._graph.getNode(id))) diff --git a/packages/core/core/src/RequestTracker.js b/packages/core/core/src/RequestTracker.js index de505da8a31..98f729b30d7 100644 --- a/packages/core/core/src/RequestTracker.js +++ b/packages/core/core/src/RequestTracker.js @@ -4,7 +4,12 @@ import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill'; import type {Async, EnvMap} from '@parcel/types'; import type {EventType, Options as WatcherOptions} from '@parcel/watcher'; import type WorkerFarm from '@parcel/workers'; -import type {ContentKey, NodeId, SerializedContentGraph} from '@parcel/graph'; +import type { + ContentGraphOpts, + ContentKey, + NodeId, + SerializedContentGraph, +} from '@parcel/graph'; import type { ParcelOptions, RequestInvalidation, @@ -55,6 +60,17 @@ export const requestGraphEdgeTypes = { }; export type RequestGraphEdgeType = $Values; + +type RequestGraphOpts = {| + ...ContentGraphOpts, + invalidNodeIds: Set, + incompleteNodeIds: Set, + globNodeIds: Set, + envNodeIds: Set, + optionNodeIds: Set, + unpredicatableNodeIds: Set, +|}; + type SerializedRequestGraph = {| ...SerializedContentGraph, invalidNodeIds: Set, @@ -201,7 +217,7 @@ export class RequestGraph extends ContentGraph< unpredicatableNodeIds: Set = new Set(); // $FlowFixMe[prop-missing] - static deserialize(opts: SerializedRequestGraph): RequestGraph { + static deserialize(opts: RequestGraphOpts): RequestGraph { // $FlowFixMe[prop-missing] let deserialized = new RequestGraph(opts); deserialized.invalidNodeIds = opts.invalidNodeIds; diff --git a/packages/core/core/test/AssetGraph.test.js b/packages/core/core/test/AssetGraph.test.js index 4f2a246f686..5f5fc249f1a 100644 --- a/packages/core/core/test/AssetGraph.test.js +++ b/packages/core/core/test/AssetGraph.test.js @@ -150,7 +150,7 @@ describe('AssetGraph', () => { }).id, ), ); - assert.deepEqual(graph.getAllEdges(), [ + assert.deepEqual(Array.from(graph.getAllEdges()), [ { from: graph.rootNodeId, to: graph.getNodeIdByContentKey('entry_specifier:path/to/index1'), diff --git a/packages/core/graph/src/AdjacencyList.js b/packages/core/graph/src/AdjacencyList.js new file mode 100644 index 00000000000..6f8d4c28f4b --- /dev/null +++ b/packages/core/graph/src/AdjacencyList.js @@ -0,0 +1,1211 @@ +// @flow +import assert from 'assert'; +import nullthrows from 'nullthrows'; +import {fromNodeId, toNodeId} from './types'; +import {ALL_EDGE_TYPES, type NullEdgeType, type AllEdgeTypes} from './Graph'; +import type {NodeId} from './types'; + +/** The address of the node in the nodes map. */ +opaque type NodeAddress = number; + +opaque type EdgeHash = number; + +/** The address of the edge in the edges map. */ +opaque type EdgeAddress = number; + +// eslint-disable-next-line no-unused-vars +export type SerializedAdjacencyList = {| + nodes: Uint32Array, + edges: Uint32Array, +|}; + +// eslint-disable-next-line no-unused-vars +export type AdjacencyListOptions = {| + edgeCapacity?: number, + nodeCapacity?: number, +|}; + +/** The upper bound above which capacity should be increased. */ +const LOAD_FACTOR = 0.7; +/** The lower bound below which capacity should be decreased. */ +const UNLOAD_FACTOR = 0.3; +/** The max amount by which to grow the capacity. */ +const MAX_GROW_FACTOR = 8; +/** The min amount by which to grow the capacity. */ +const MIN_GROW_FACTOR = 2; +/** The amount by which to shrink the capacity. */ +const SHRINK_FACTOR = 0.5; + +export default class AdjacencyList { + #nodes /*: NodeTypeMap */; + #edges /*: EdgeTypeMap */; + + constructor( + opts?: + | SerializedAdjacencyList + | AdjacencyListOptions, + ) { + let nodes; + let edges; + + if (opts?.nodes) { + ({nodes, edges} = opts); + this.#nodes = new NodeTypeMap(nodes); + this.#edges = new EdgeTypeMap(edges); + } else { + let { + nodeCapacity = NodeTypeMap.MIN_CAPACITY, + edgeCapacity = EdgeTypeMap.MIN_CAPACITY, + } = opts ?? {}; + assert( + nodeCapacity <= NodeTypeMap.MAX_CAPACITY, + 'Node capacity overflow!', + ); + assert( + edgeCapacity <= EdgeTypeMap.MAX_CAPACITY, + 'Edge capacity overflow!', + ); + this.#nodes = new NodeTypeMap(nodeCapacity); + this.#edges = new EdgeTypeMap(edgeCapacity); + } + } + + /** + * Create a new `AdjacencyList` from the given options. + */ + static deserialize( + opts: SerializedAdjacencyList, + ): AdjacencyList { + return new AdjacencyList(opts); + } + + /** + * Returns a serializable object of the nodes and edges in the graph. + */ + serialize(): SerializedAdjacencyList { + return { + nodes: this.#nodes.data, + edges: this.#edges.data, + }; + } + + get stats(): {| + /** The number of nodes in the graph. */ + nodes: number, + /** The number of edge types associated with nodes in the graph. */ + nodeEdgeTypes: number, + /** The maximum number of nodes the graph can contain. */ + nodeCapacity: number, + /** The size of the raw nodes buffer, in mb. */ + nodeBufferSize: string, + /** The current load on the nodes array. */ + nodeLoad: string, + /** The number of edges in the graph. */ + edges: number, + /** The number of edges deleted from the graph. */ + deleted: number, + /** The maximum number of edges the graph can contain. */ + edgeCapacity: number, + /** The size of the raw edges buffer, in mb. */ + edgeBufferSize: string, + /** The current load on the edges array, including deletes. */ + edgeLoadWithDeletes: string, + /** The current load on the edges array. */ + edgeLoad: string, + /** The total number of edge hash collisions. */ + collisions: number, + /** The number of collisions for the most common hash. */ + maxCollisions: number, + /** The average number of collisions per hash. */ + avgCollisions: number, + /** The likelihood of uniform distribution. ~1.0 indicates certainty. */ + uniformity: number, + |} { + let buckets = new Map(); + for (let {from, to, type} of this.getAllEdges()) { + let hash = this.#edges.hash(from, to, type); + let bucket = buckets.get(hash) || new Set(); + let key = `${String(from)}, ${String(to)}, ${String(type)}`; + assert(!bucket.has(key), `Duplicate node detected: ${key}`); + bucket.add(key); + buckets.set(hash, bucket); + } + + let maxCollisions = 0; + let collisions = 0; + let distribution = 0; + + for (let bucket of buckets.values()) { + maxCollisions = Math.max(maxCollisions, bucket.size - 1); + collisions += bucket.size - 1; + distribution += (bucket.size * (bucket.size + 1)) / 2; + } + + let uniformity = + distribution / + ((this.#edges.count / (2 * this.#edges.capacity)) * + (this.#edges.count + 2 * this.#edges.capacity - 1)); + + return { + nodes: fromNodeId(this.#nodes.nextId), + nodeEdgeTypes: this.#nodes.count, + nodeCapacity: this.#nodes.capacity, + nodeLoad: `${Math.round(this.#nodes.load * 100)}%`, + nodeBufferSize: this.#nodes.bufferSize, + + edges: this.#edges.count, + deleted: this.#edges.deletes, + edgeCapacity: this.#edges.capacity, + edgeLoad: `${Math.round(this.#edges.load * 100)}%`, + edgeLoadWithDeletes: `${Math.round( + this.#edges.getLoad(this.#edges.count + this.#edges.deletes) * 100, + )}%`, + edgeBufferSize: this.#edges.bufferSize, + + collisions, + maxCollisions, + avgCollisions: Math.round((collisions / buckets.size) * 100) / 100 || 0, + uniformity: Math.round(uniformity * 100) / 100 || 0, + }; + } + + /** + * Resize the internal nodes array. + * + * This is used in `addNode` when the `numNodes` meets or exceeds + * the allocated size of the `nodes` array. + */ + resizeNodes(size: number) { + let nodes = this.#nodes; + // Allocate the required space for a `nodes` map of the given `size`. + this.#nodes = new NodeTypeMap(size); + // Copy the existing nodes into the new array. + this.#nodes.set(nodes.data); + } + + /** + * Resize the internal edges array. + * + * This is used in `addEdge` when the `numEdges` meets or exceeds + * the allocated size of the `edges` array. + */ + resizeEdges(size: number) { + // Allocate the required space for new `nodes` and `edges` maps. + let copy = new AdjacencyList({ + nodeCapacity: this.#nodes.capacity, + edgeCapacity: size, + }); + + // Copy the existing edges into the new array. + copy.#nodes.nextId = this.#nodes.nextId; + this.#edges.forEach( + edge => + void copy.addEdge( + this.#edges.from(edge), + this.#edges.to(edge), + this.#edges.typeOf(edge), + ), + ); + + // We expect to preserve the same number of edges. + assert( + this.#edges.count === copy.#edges.count, + `Edge mismatch! ${this.#edges.count} does not match ${ + copy.#edges.count + }.`, + ); + + // Finally, copy the new data arrays over to this graph. + this.#nodes = copy.#nodes; + this.#edges = copy.#edges; + } + + /** + * Adds a node to the graph. + * + * Returns the id of the added node. + */ + addNode(): NodeId { + let id = this.#nodes.getId(); + // If we're in danger of overflowing the `nodes` array, resize it. + if (this.#nodes.load > LOAD_FACTOR) { + this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity)); + } + return id; + } + + /** + * Adds an edge to the graph. + * + * Returns `true` if the edge was added, + * or `false` if the edge already exists. + */ + addEdge( + from: NodeId, + to: NodeId, + type: TEdgeType | NullEdgeType = 1, + ): boolean { + assert(type > 0, `Unsupported edge type ${0}`); + + let hash = this.#edges.hash(from, to, type); + let edge = this.#edges.addressOf(hash, from, to, type); + + // The edge is already in the graph; do nothing. + if (edge !== null) return false; + + let capacity = this.#edges.capacity; + // We add 1 to account for the edge we are adding. + let count = this.#edges.count + 1; + // Since the space occupied by deleted edges isn't reclaimed, + // we include them in our count to avoid overflowing the `edges` array. + let deletes = this.#edges.deletes; + let total = count + deletes; + // If we have enough space to keep adding edges, we can + // put off reclaiming the deleted space until the next resize. + if (this.#edges.getLoad(total) > LOAD_FACTOR) { + if (this.#edges.getLoad(deletes) > UNLOAD_FACTOR) { + // If we have a significant number of deletes, we compute our new + // capacity based on the current count, even though we decided to + // resize based on the sum total of count and deletes. + // In this case, resizing is more like a compaction. + this.resizeEdges( + getNextEdgeCapacity(capacity, count, this.#edges.getLoad(count)), + ); + } else { + this.resizeEdges( + getNextEdgeCapacity(capacity, total, this.#edges.getLoad(total)), + ); + } + // We must rehash because the capacity has changed. + hash = this.#edges.hash(from, to, type); + } + + let toNode = this.#nodes.addressOf(to, type); + let fromNode = this.#nodes.addressOf(from, type); + if (toNode === null || fromNode === null) { + // If we're in danger of overflowing the `nodes` array, resize it. + if (this.#nodes.load >= LOAD_FACTOR) { + this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity)); + // We need to update our indices since the `nodes` array has changed. + toNode = this.#nodes.addressOf(to, type); + fromNode = this.#nodes.addressOf(from, type); + } + } + if (toNode === null) toNode = this.#nodes.add(to, type); + if (fromNode === null) fromNode = this.#nodes.add(from, type); + + // Add our new edge to its hash bucket. + edge = this.#edges.add(hash, from, to, type); + + // Link this edge to the node's list of incoming edges. + let prevIn = this.#nodes.linkIn(toNode, edge); + if (prevIn !== null) this.#edges.linkIn(prevIn, edge); + + // Link this edge to the node's list of outgoing edges. + let prevOut = this.#nodes.linkOut(fromNode, edge); + if (prevOut !== null) this.#edges.linkOut(prevOut, edge); + + return true; + } + + *getAllEdges(): Iterator<{| + type: TEdgeType | NullEdgeType, + from: NodeId, + to: NodeId, + |}> { + for (let edge of this.#edges) { + yield { + from: this.#edges.from(edge), + to: this.#edges.to(edge), + type: this.#edges.typeOf(edge), + }; + } + } + + /** + * Check if the graph has an edge connecting the `from` and `to` nodes. + */ + hasEdge( + from: NodeId, + to: NodeId, + type: TEdgeType | NullEdgeType = 1, + ): boolean { + let hash = this.#edges.hash(from, to, type); + return this.#edges.addressOf(hash, from, to, type) !== null; + } + + /** + * + */ + removeEdge( + from: NodeId, + to: NodeId, + type: TEdgeType | NullEdgeType = 1, + ): void { + let hash = this.#edges.hash(from, to, type); + let edge = this.#edges.addressOf(hash, from, to, type); + + // The edge is not in the graph; do nothing. + if (edge === null) return; + + let toNode = nullthrows(this.#nodes.addressOf(to, type)); + let fromNode = nullthrows(this.#nodes.addressOf(from, type)); + + // Update the terminating node's first and last incoming edges. + this.#nodes.unlinkIn( + toNode, + edge, + this.#edges.prevIn(edge), + this.#edges.nextIn(edge), + ); + + // Update the originating node's first and last outgoing edges. + this.#nodes.unlinkOut( + fromNode, + edge, + this.#edges.prevOut(edge), + this.#edges.nextOut(edge), + ); + + // Splice the removed edge out of the linked list of edges in the bucket. + this.#edges.unlink(hash, edge); + // Splice the removed edge out of the linked list of incoming edges. + this.#edges.unlinkIn(edge); + // Splice the removed edge out of the linked list of outgoing edges. + this.#edges.unlinkOut(edge); + // Finally, delete the edge. + this.#edges.delete(edge); + } + + hasInboundEdges(to: NodeId): boolean { + let node = this.#nodes.head(to); + while (node !== null) { + if (this.#nodes.firstIn(node) !== null) return true; + node = this.#nodes.next(node); + } + return false; + } + + getInboundEdgesByType( + to: NodeId, + ): {|type: TEdgeType | NullEdgeType, from: NodeId|}[] { + let edges = []; + let node = this.#nodes.head(to); + while (node !== null) { + let type = this.#nodes.typeOf(node); + let edge = this.#nodes.firstIn(node); + while (edge !== null) { + let from = this.#edges.from(edge); + edges.push({from, type}); + edge = this.#edges.nextIn(edge); + } + node = this.#nodes.next(node); + } + return edges; + } + + getOutboundEdgesByType( + from: NodeId, + ): {|type: TEdgeType | NullEdgeType, to: NodeId|}[] { + let edges = []; + let node = this.#nodes.head(from); + while (node !== null) { + let type = this.#nodes.typeOf(node); + let edge = this.#nodes.firstOut(node); + while (edge !== null) { + let to = this.#edges.to(edge); + edges.push({to, type}); + edge = this.#edges.nextOut(edge); + } + node = this.#nodes.next(node); + } + return edges; + } + + /** + * Get the list of nodes connected from this node. + */ + getNodeIdsConnectedFrom( + from: NodeId, + type: + | AllEdgeTypes + | TEdgeType + | NullEdgeType + | Array = 1, + ): NodeId[] { + let matches = node => + type === ALL_EDGE_TYPES || + (Array.isArray(type) + ? type.includes(this.#nodes.typeOf(node)) + : type === this.#nodes.typeOf(node)); + + let nodes = []; + let node = this.#nodes.head(from); + while (node !== null) { + if (matches(node)) { + let edge = this.#nodes.firstOut(node); + while (edge !== null) { + nodes.push(this.#edges.to(edge)); + edge = this.#edges.nextOut(edge); + } + } + node = this.#nodes.next(node); + } + return nodes; + } + + /** + * Get the list of nodes connected to this node. + */ + getNodeIdsConnectedTo( + to: NodeId, + type: + | AllEdgeTypes + | TEdgeType + | NullEdgeType + | Array = 1, + ): NodeId[] { + let matches = node => + type === ALL_EDGE_TYPES || + (Array.isArray(type) + ? type.includes(this.#nodes.typeOf(node)) + : type === this.#nodes.typeOf(node)); + + let nodes = []; + let node = this.#nodes.head(to); + while (node !== null) { + if (matches(node)) { + let edge = this.#nodes.firstIn(node); + while (edge !== null) { + nodes.push(this.#edges.from(edge)); + edge = this.#edges.nextIn(edge); + } + } + node = this.#nodes.next(node); + } + return nodes; + } + + inspect(): any { + return { + nodes: this.#nodes.inspect(), + edges: this.#edges.inspect(), + }; + } +} + +/** + * `SharedTypeMap` is a hashmap of items, + * where each item has its own 'type' field. + * + * The `SharedTypeMap` is backed by a shared array buffer of fixed length. + * The buffer is partitioned into: + * - a header, which stores the capacity and number of items in the map, + * - a hash table, which is an array of pointers to linked lists of items + * with the same hash, + * - an items array, which is where the linked items are stored. + * + * hash table item + * (capacity) (ITEM_SIZE) + * ┌──────┴──────┐ ┌──┴──┐ + * ┌──┬──┬──┬───────┬──┬──┬──┬───────┬──┬──┐ + * │ │ │ │ ... │ │ │ │ ... │ │ │ + * └──┴──┴──┴───────┴──┴──┴──┴───────┴──┴──┘ + * └──┬──┘ └─────────┬─────────┘ + * header items + * (HEADER_SIZE) (capacity * ITEM_SIZE * BUCKET_SIZE) + * + * + * An item is added with a hash key that fits within the range of the hash + * table capacity. The item is stored at the next available address after the + * hash table, and a pointer to the address is stored in the hash table at + * the index matching the hash. If the hash is already pointing at an item, + * the pointer is stored in the `next` field of the existing item instead. + * + * hash table items + * ┌─────────┴────────┐┌───────────────────────┴────────────────────────┐ + * 0 1 2 11 17 23 29 35 + * ┌───┐┌───┐┌───┐┌───┐┌───┬───┐┌───┬───┐┌───┬───┐┌───┬───┐┌───┬───┐┌───┐ + * │17 ││11 ││35 ││...││23 │ 1 ││29 │ 1 ││ 0 │ 2 ││ 0 │ 2 ││ 0 │ 1 ││...│ + * └───┘└───┘└───┘└───┘└───┴───┘└───┴───┘└───┴───┘└───┴───┘└───┴───┘└───┘ + * │ │ │ ▲ ▲ ▲ ▲ ▲ + * └────┼────┼─────────┼────────┴────────┼────────┘ │ + * └────┼─────────┴─────────────────┘ │ + * └─────────────────────────────────────────────┘ + */ +export class SharedTypeMap + implements Iterable +{ + /** + * The header for the `SharedTypeMap` comprises 2 4-byte chunks: + * + * struct SharedTypeMapHeader { + * int capacity; + * int count; + * } + * + * ┌──────────┬───────┐ + * │ CAPACITY │ COUNT │ + * └──────────┴───────┘ + */ + static HEADER_SIZE: number = 2; + /** The offset from the header where the capacity is stored. */ + static #CAPACITY: 0 = 0; + /** The offset from the header where the count is stored. */ + static #COUNT: 1 = 1; + + /** + * Each item in `SharedTypeMap` comprises 2 4-byte chunks: + * + * struct Node { + * int next; + * int type; + * } + * + * ┌──────┬──────┐ + * │ NEXT │ TYPE │ + * └──────┴──────┘ + */ + static ITEM_SIZE: number = 2; + /** The offset at which a link to the next item in the same bucket is stored. */ + static #NEXT: 0 = 0; + /** The offset at which an item's type is stored. */ + static #TYPE: 1 = 1; + + /** The number of items to accommodate per hash bucket. */ + static BUCKET_SIZE: number = 2; + + data: Uint32Array; + + get capacity(): number { + return this.data[SharedTypeMap.#CAPACITY]; + } + + get count(): number { + return this.data[SharedTypeMap.#COUNT]; + } + + get load(): number { + return this.getLoad(); + } + + get length(): number { + return this.getLength(); + } + + get addressableLimit(): number { + return this.constructor.HEADER_SIZE + this.capacity; + } + + get bufferSize(): string { + return `${(this.data.byteLength / 1024 / 1024).toLocaleString(undefined, { + minmumFractionDigits: 2, + maximumFractionDigits: 2, + })} mb`; + } + + constructor(capacityOrData: number | Uint32Array) { + if (typeof capacityOrData === 'number') { + let {BYTES_PER_ELEMENT} = Uint32Array; + let CAPACITY = SharedTypeMap.#CAPACITY; + // $FlowFixMe[incompatible-call] + this.data = new Uint32Array( + new SharedArrayBuffer( + this.getLength(capacityOrData) * BYTES_PER_ELEMENT, + ), + ); + this.data[CAPACITY] = capacityOrData; + } else { + this.data = capacityOrData; + assert(this.getLength() === this.data.length, 'Data appears corrupt.'); + } + } + + set(data: Uint32Array): void { + let {HEADER_SIZE, ITEM_SIZE} = this.constructor; + let NEXT = SharedTypeMap.#NEXT; + let COUNT = SharedTypeMap.#COUNT; + let CAPACITY = SharedTypeMap.#CAPACITY; + + let delta = this.capacity - data[CAPACITY]; + assert(delta >= 0, 'Cannot copy to a map with smaller capacity.'); + + // Copy the header. + this.data.set(data.subarray(COUNT, HEADER_SIZE), COUNT); + + // Copy the hash table. + let toTable = this.data.subarray(HEADER_SIZE, HEADER_SIZE + this.capacity); + toTable.set(data.subarray(HEADER_SIZE, HEADER_SIZE + data[CAPACITY])); + // Offset first links to account for the change in table capacity. + let max = toTable.length; + for (let i = 0; i < max; i++) { + if (toTable[i]) toTable[i] += delta; + } + + // Copy the items. + let toItems = this.data.subarray(HEADER_SIZE + this.capacity); + toItems.set(data.subarray(HEADER_SIZE + data[CAPACITY])); + // Offset next links to account for the change in table capacity. + max = toItems.length; + for (let i = 0; i < max; i += ITEM_SIZE) { + if (toItems[i + NEXT]) toItems[i + NEXT] += delta; + } + } + + getLoad(count: number = this.count): number { + let {BUCKET_SIZE} = this.constructor; + return count / (this.capacity * BUCKET_SIZE); + } + + getLength(capacity: number = this.capacity): number { + let {HEADER_SIZE, ITEM_SIZE, BUCKET_SIZE} = this.constructor; + return capacity + HEADER_SIZE + ITEM_SIZE * BUCKET_SIZE * capacity; + } + + /** Get the next available address in the map. */ + getNextAddress(): TAddress { + let {HEADER_SIZE, ITEM_SIZE} = this.constructor; + return (HEADER_SIZE + this.capacity + this.count * ITEM_SIZE: any); + } + + /** Get the address of the first item with the given hash. */ + head(hash: THash): TAddress | null { + let {HEADER_SIZE} = this.constructor; + return (this.data[HEADER_SIZE + (hash: any)]: any) || null; + } + + /** Get the address of the next item with the same hash as the given item. */ + next(item: TAddress): TAddress | null { + let NEXT = SharedTypeMap.#NEXT; + return (this.data[(item: any) + NEXT]: any) || null; + } + + typeOf(item: TAddress): TItemType { + return (this.data[item + SharedTypeMap.#TYPE]: any); + } + + link(hash: THash, item: TAddress, type: TItemType): void { + let COUNT = SharedTypeMap.#COUNT; + let NEXT = SharedTypeMap.#NEXT; + let TYPE = SharedTypeMap.#TYPE; + let {HEADER_SIZE} = this.constructor; + + this.data[item + TYPE] = (type: any); + + let prev = this.head(hash); + if (prev !== null) { + let next = this.next(prev); + while (next !== null) { + prev = next; + next = this.next(next); + } + this.data[prev + NEXT] = item; + } else { + // This is the first item in the bucket! + this.data[HEADER_SIZE + (hash: any)] = item; + } + this.data[COUNT]++; + } + + unlink(hash: THash, item: TAddress): void { + let COUNT = SharedTypeMap.#COUNT; + let NEXT = SharedTypeMap.#NEXT; + let TYPE = SharedTypeMap.#TYPE; + let {HEADER_SIZE} = this.constructor; + + this.data[item + TYPE] = 0; + + let head = this.head(hash); + // No bucket to unlink from. + if (head === null) return; + + let next = this.next(item); + let prev = null; + let candidate = head; + while (candidate !== null && candidate !== item) { + prev = candidate; + candidate = this.next(candidate); + } + if (prev !== null && next !== null) { + this.data[prev + NEXT] = next; + } else if (prev !== null) { + this.data[prev + NEXT] = 0; + } else if (next !== null) { + this.data[HEADER_SIZE + (hash: any)] = next; + } else { + this.data[HEADER_SIZE + (hash: any)] = 0; + } + this.data[item + NEXT] = 0; + this.data[COUNT]--; + } + + forEach(cb: (item: TAddress) => void): void { + let max = this.count; + let len = this.length; + let {ITEM_SIZE} = this.constructor; + for ( + let i = this.addressableLimit, count = 0; + i < len && count < max; + i += ITEM_SIZE + ) { + // Skip items that don't have a type. + if (this.typeOf((i: any))) { + cb((i: any)); + count++; + } + } + } + + // Trick Flow into believing in `Symbol.iterator`. + // See https://github.com/facebook/flow/issues/1163#issuecomment-353523840 + /*:: @@iterator(): Iterator { return ({}: any); } */ + // $FlowFixMe[unsupported-syntax] + *[Symbol.iterator](): Iterator { + let max = this.count; + let len = this.length; + let {ITEM_SIZE} = this.constructor; + for ( + let i = this.addressableLimit, count = 0; + i < len && count < max; + i += ITEM_SIZE + ) { + if (this.data.subarray(i, i + ITEM_SIZE).some(Boolean)) { + yield (i: any); + count++; + } + } + } + + inspect(): {| + header: Uint32Array, + table: Uint32Array, + data: Uint32Array, + |} { + const {HEADER_SIZE, ITEM_SIZE, BUCKET_SIZE} = this.constructor; + let min = HEADER_SIZE + this.capacity; + let max = min + this.capacity * BUCKET_SIZE * ITEM_SIZE; + return { + header: this.data.subarray(0, HEADER_SIZE), + table: this.data.subarray(HEADER_SIZE, min), + data: this.data.subarray(min, max), + }; + } +} + +/** + * Nodes are stored in a `SharedTypeMap`, keyed on node id plus an edge type. + * This means that for any given unique node id, there may be `e` nodes in the + * map, where `e` is the number of possible edge types in the graph. + */ +export class NodeTypeMap extends SharedTypeMap< + TEdgeType, + NodeId, + NodeAddress, +> { + /** + * In addition to the header defined by `SharedTypeMap`, the header for + * the node map includes a 4-byte `nextId` chunk: + * + * struct NodeTypeMapHeader { + * int capacity; // from `SharedTypeMap` + * int count; // from `SharedTypeMap` + * int nextId; + * } + * + * ┌──────────┬───────┬─────────┐ + * │ CAPACITY │ COUNT │ NEXT_ID │ + * └──────────┴───────┴─────────┘ + */ + static HEADER_SIZE: number = 3; + /** The offset from the header where the next available node id is stored. */ + static #NEXT_ID = 2; + + /** + * In addition to the item fields defined by `SharedTypeMap`, + * each node includes another 4 4-byte chunks: + * + * struct Node { + * int next; // from `SharedTypeMap` + * int type; // from `SharedTypeMap` + * int firstIn; + * int firstOut; + * int lastIn; + * int lastOut; + * } + * + * ┌──────┬──────┬──────────┬───────────┬─────────┬──────────┐ + * │ NEXT │ TYPE │ FIRST_IN │ FIRST_OUT │ LAST_IN │ LAST_OUT │ + * └──────┴──────┴──────────┴───────────┴─────────┴──────────┘ + */ + static ITEM_SIZE: number = 6; + /** The offset at which a node's first incoming edge of this type is stored. */ + static #FIRST_IN = 2; + /** The offset at which a node's first outgoing edge of this type is stored. */ + static #FIRST_OUT = 3; + /** The offset at which a node's last incoming edge of this type is stored. */ + static #LAST_IN = 4; + /** The offset at which a node's last outgoing edge of this type is stored. */ + static #LAST_OUT = 5; + + /** The smallest functional node map capacity. */ + static MIN_CAPACITY: number = 2; + /** The largest possible node map capacity. */ + static MAX_CAPACITY: number = Math.floor( + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong + (2 ** 31 - 1 - NodeTypeMap.HEADER_SIZE) / + NodeTypeMap.ITEM_SIZE / + NodeTypeMap.BUCKET_SIZE, + ); + + get nextId(): NodeId { + return toNodeId(this.data[NodeTypeMap.#NEXT_ID]); + } + set nextId(nextId: NodeId) { + this.data[NodeTypeMap.#NEXT_ID] = fromNodeId(nextId); + } + + /** Get a unique node id. */ + getId(): NodeId { + return toNodeId(this.data[NodeTypeMap.#NEXT_ID]++); + } + + getLoad(count: number = this.count): number { + return Math.max( + fromNodeId(this.nextId) / this.capacity, + super.getLoad(count), + ); + } + + add(node: NodeId, type: TEdgeType): NodeAddress { + let index = fromNodeId(node); + assert( + index >= 0 && index < this.data[NodeTypeMap.#NEXT_ID], + `Invalid node id ${String(node)} (${this.data[NodeTypeMap.#NEXT_ID]})`, + ); + let address = this.getNextAddress(); + this.link(node, address, type); + return address; + } + + addressOf(node: NodeId, type: TEdgeType): NodeAddress | null { + let address = this.head(node); + while (address !== null) { + if (this.typeOf(address) === type) { + return address; + } + address = this.next(address); + } + return null; + } + + firstIn(node: NodeAddress): EdgeAddress | null { + return this.data[node + NodeTypeMap.#FIRST_IN] || null; + } + + firstOut(node: NodeAddress): EdgeAddress | null { + return this.data[node + NodeTypeMap.#FIRST_OUT] || null; + } + + lastIn(node: NodeAddress): EdgeAddress | null { + return this.data[node + NodeTypeMap.#LAST_IN] || null; + } + + lastOut(node: NodeAddress): EdgeAddress | null { + return this.data[node + NodeTypeMap.#LAST_OUT] || null; + } + + linkIn(node: NodeAddress, edge: EdgeAddress): EdgeAddress | null { + let first = this.firstIn(node); + let last = this.lastIn(node); + if (first === null) this.data[node + NodeTypeMap.#FIRST_IN] = edge; + this.data[node + NodeTypeMap.#LAST_IN] = edge; + return last; + } + + unlinkIn( + node: NodeAddress, + edge: EdgeAddress, + prev: EdgeAddress | null, + next: EdgeAddress | null, + ): void { + let first = this.firstIn(node); + let last = this.lastIn(node); + if (last === edge) { + this.data[node + NodeTypeMap.#LAST_IN] = prev === null ? 0 : prev; + } + if (first === edge) { + this.data[node + NodeTypeMap.#FIRST_IN] = next === null ? 0 : next; + } + } + + linkOut(node: NodeAddress, edge: EdgeAddress): EdgeAddress | null { + let first = this.firstOut(node); + let last = this.lastOut(node); + if (first === null) this.data[node + NodeTypeMap.#FIRST_OUT] = edge; + this.data[node + NodeTypeMap.#LAST_OUT] = edge; + return last; + } + + unlinkOut( + node: NodeAddress, + edge: EdgeAddress, + prev: EdgeAddress | null, + next: EdgeAddress | null, + ): void { + let first = this.firstOut(node); + let last = this.lastOut(node); + if (last === edge) { + this.data[node + NodeTypeMap.#LAST_OUT] = prev === null ? 0 : prev; + } + if (first === edge) { + this.data[node + NodeTypeMap.#FIRST_OUT] = next === null ? 0 : next; + } + } +} + +/** + * Edges are stored in a `SharedTypeMap`, + * keyed on the 'from' and 'to' node ids, and the edge type. + */ +export class EdgeTypeMap extends SharedTypeMap< + TEdgeType, + EdgeHash, + EdgeAddress, +> { + /** + * In addition to the header defined by `SharedTypeMap`, the header for + * the edge map includes a 4-byte `deletes` chunk: + * + * struct EdgeTypeMapHeader { + * int capacity; // from `SharedTypeMap` + * int count; // from `SharedTypeMap` + * int deletes; + * } + * + * ┌──────────┬───────┬─────────┐ + * │ CAPACITY │ COUNT │ DELETES │ + * └──────────┴───────┴─────────┘ + */ + static HEADER_SIZE: number = 3; + /** The offset from the header where the delete count is stored. */ + static #DELETES = 2; + + /** + * In addition to the item fields defined by `SharedTypeMap`, + * each edge includes another 6 4-byte chunks: + * + * struct Edge { + * int next; // from `SharedTypeMap` + * int type; // from `SharedTypeMap` + * int from; + * int to; + * int nextIn; + * int prevIn; + * int nextOut; + * int prevOut; + * } + * + * ┌──────┬──────┬──────┬────┬─────────┬─────────┬──────────┬──────────┐ + * │ NEXT │ TYPE │ FROM │ TO │ NEXT_IN │ PREV_IN │ NEXT_OUT │ PREV_OUT │ + * └──────┴──────┴──────┴────┴─────────┴─────────┴──────────┴──────────┘ + */ + static ITEM_SIZE: number = 8; + /** The offset at which an edge's 'from' node id is stored. */ + static #FROM = 2; + /** The offset at which an edge's 'to' node id is stored. */ + static #TO = 3; + /** The offset at which the 'to' node's next incoming edge is stored. */ + static #NEXT_IN = 4; + /** The offset at which the 'to' node's previous incoming edge is stored. */ + static #PREV_IN = 5; + /** The offset at which the 'from' node's next outgoing edge is stored. */ + static #NEXT_OUT = 6; + /** The offset at which the 'from' node's previous outgoing edge is stored. */ + static #PREV_OUT = 7; + + /** The smallest functional edge map capacity. */ + static MIN_CAPACITY: number = 2; + /** The largest possible edge map capacity. */ + static MAX_CAPACITY: number = Math.floor( + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong + (2 ** 31 - 1 - EdgeTypeMap.HEADER_SIZE) / + EdgeTypeMap.ITEM_SIZE / + EdgeTypeMap.BUCKET_SIZE, + ); + /** The size after which to grow the capacity by the minimum factor. */ + static PEAK_CAPACITY: number = 2 ** 18; + + get deletes(): number { + return this.data[EdgeTypeMap.#DELETES]; + } + + getNextAddress(): EdgeAddress { + let {ITEM_SIZE} = this.constructor; + return this.addressableLimit + (this.count + this.deletes) * ITEM_SIZE; + } + + add(hash: EdgeHash, from: NodeId, to: NodeId, type: TEdgeType): EdgeAddress { + assert( + hash >= 0 && hash < this.capacity, + `Invalid edge hash ${String(hash)}`, + ); + // Use the next available edge address. + let edge = this.getNextAddress(); + // Add our new edge to its hash bucket. + this.link(hash, edge, type); + this.data[edge + EdgeTypeMap.#FROM] = fromNodeId(from); + this.data[edge + EdgeTypeMap.#TO] = fromNodeId(to); + return edge; + } + + delete(edge: EdgeAddress): void { + this.data[edge + EdgeTypeMap.#FROM] = 0; + this.data[edge + EdgeTypeMap.#TO] = 0; + this.data[EdgeTypeMap.#DELETES]++; + } + + addressOf( + hash: EdgeHash, + from: NodeId, + to: NodeId, + type: TEdgeType, + ): EdgeAddress | null { + let address = this.head(hash); + while (address !== null) { + if ( + this.typeOf(address) === type && + this.from(address) === from && + this.to(address) === to + ) { + return address; + } + address = this.next(address); + } + return null; + } + + from(edge: EdgeAddress): NodeId { + return toNodeId(this.data[edge + EdgeTypeMap.#FROM]); + } + + to(edge: EdgeAddress): NodeId { + return toNodeId(this.data[edge + EdgeTypeMap.#TO]); + } + + nextIn(edge: EdgeAddress): EdgeAddress | null { + return this.data[edge + EdgeTypeMap.#NEXT_IN] || null; + } + + prevIn(edge: EdgeAddress): EdgeAddress | null { + return this.data[edge + EdgeTypeMap.#PREV_IN] || null; + } + + linkIn(edge: EdgeAddress, next: EdgeAddress) { + this.data[edge + EdgeTypeMap.#NEXT_IN] = next; + this.data[next + EdgeTypeMap.#PREV_IN] = edge; + } + + unlinkIn(edge: EdgeAddress) { + let next = this.nextIn(edge); + let prev = this.prevIn(edge); + this.data[edge + EdgeTypeMap.#NEXT_IN] = 0; + this.data[edge + EdgeTypeMap.#PREV_IN] = 0; + if (next !== null && prev !== null) { + this.data[prev + EdgeTypeMap.#NEXT_IN] = next; + this.data[next + EdgeTypeMap.#PREV_IN] = prev; + } else if (next !== null) { + this.data[next + EdgeTypeMap.#PREV_IN] = 0; + } else if (prev !== null) { + this.data[prev + EdgeTypeMap.#NEXT_IN] = 0; + } + } + + nextOut(edge: EdgeAddress): EdgeAddress | null { + return this.data[edge + EdgeTypeMap.#NEXT_OUT] || null; + } + + prevOut(edge: EdgeAddress): EdgeAddress | null { + return this.data[edge + EdgeTypeMap.#PREV_OUT] || null; + } + + linkOut(edge: EdgeAddress, next: EdgeAddress) { + this.data[edge + EdgeTypeMap.#NEXT_OUT] = next; + this.data[next + EdgeTypeMap.#PREV_OUT] = edge; + } + + unlinkOut(edge: EdgeAddress) { + let next = this.nextOut(edge); + let prev = this.prevOut(edge); + this.data[edge + EdgeTypeMap.#NEXT_OUT] = 0; + this.data[edge + EdgeTypeMap.#PREV_OUT] = 0; + if (next !== null && prev !== null) { + this.data[prev + EdgeTypeMap.#NEXT_OUT] = next; + this.data[next + EdgeTypeMap.#PREV_OUT] = prev; + } else if (next !== null) { + this.data[next + EdgeTypeMap.#PREV_OUT] = 0; + } else if (prev !== null) { + this.data[prev + EdgeTypeMap.#NEXT_OUT] = 0; + } + } + + /** Create a hash of the edge connecting the `from` and `to` nodes. */ + hash(from: NodeId, to: NodeId, type: TEdgeType): EdgeHash { + // Each parameter is hashed by mixing its upper bits into its lower bits to + // increase the likelihood that a change to any bit of the input will vary + // the output widely. Then we do a series of prime multiplications and + // additions to combine the hashes into one value. + let hash = 17; + hash = hash * 37 + hash32shift((from: any)); + hash = hash * 37 + hash32shift((to: any)); + hash = hash * 37 + hash32shift((type: any)); + // Finally, we map the hash to a value modulo the edge capacity. + hash %= this.capacity; + return hash; + } +} + +// From https://gist.github.com/badboy/6267743#32-bit-mix-functions +function hash32shift(key: number): number { + key = ~key + (key << 15); // key = (key << 15) - key - 1; + key = key ^ (key >> 12); + key = key + (key << 2); + key = key ^ (key >> 4); + key = key * 2057; // key = (key + (key << 3)) + (key << 11); + key = key ^ (key >> 16); + return key; +} + +function interpolate(x: number, y: number, t: number): number { + return x + (y - x) * Math.min(1, Math.max(0, t)); +} + +function increaseNodeCapacity(nodeCapacity: number): number { + let {MIN_CAPACITY, MAX_CAPACITY} = NodeTypeMap; + let newCapacity = Math.round(nodeCapacity * MIN_GROW_FACTOR); + assert(newCapacity <= MAX_CAPACITY, 'Node capacity overflow!'); + return Math.max(MIN_CAPACITY, newCapacity); +} + +function getNextEdgeCapacity( + capacity: number, + count: number, + load: number, +): number { + let {MIN_CAPACITY, MAX_CAPACITY, PEAK_CAPACITY} = EdgeTypeMap; + let newCapacity = capacity; + if (load > LOAD_FACTOR) { + // This is intended to strike a balance between growing the edge capacity + // in too small increments, which causes a lot of resizing, and growing + // the edge capacity in too large increments, which results in a lot of + // wasted memory. + let pct = capacity / PEAK_CAPACITY; + let growFactor = interpolate(MAX_GROW_FACTOR, MIN_GROW_FACTOR, pct); + newCapacity = Math.round(capacity * growFactor); + } else if (load < UNLOAD_FACTOR) { + // In some cases, it may be possible to shrink the edge capacity, + // but this is only likely to occur when a lot of edges have been removed. + newCapacity = Math.round(capacity * SHRINK_FACTOR); + } + assert(newCapacity <= MAX_CAPACITY, 'Edge capacity overflow!'); + return Math.max(MIN_CAPACITY, newCapacity); +} diff --git a/packages/core/graph/src/ContentGraph.js b/packages/core/graph/src/ContentGraph.js index e43da92cddc..3b8d386fbf7 100644 --- a/packages/core/graph/src/ContentGraph.js +++ b/packages/core/graph/src/ContentGraph.js @@ -1,14 +1,18 @@ // @flow strict-local import type {ContentKey, NodeId} from './types'; -import Graph, {type GraphOpts} from './Graph'; +import Graph, {type SerializedGraph, type GraphOpts} from './Graph'; import nullthrows from 'nullthrows'; -export type SerializedContentGraph = {| +export type ContentGraphOpts = {| ...GraphOpts, _contentKeyToNodeId: Map, _nodeIdToContentKey: Map, |}; +export type SerializedContentGraph = {| + ...SerializedGraph, + _contentKeyToNodeId: Map, +|}; export default class ContentGraph extends Graph< TNode, @@ -17,7 +21,7 @@ export default class ContentGraph extends Graph< _contentKeyToNodeId: Map; _nodeIdToContentKey: Map; - constructor(opts: ?SerializedContentGraph) { + constructor(opts: ?ContentGraphOpts) { if (opts) { let {_contentKeyToNodeId, _nodeIdToContentKey, ...rest} = opts; super(rest); @@ -32,13 +36,14 @@ export default class ContentGraph extends Graph< // $FlowFixMe[prop-missing] static deserialize( - opts: SerializedContentGraph, + opts: ContentGraphOpts, ): ContentGraph { return new ContentGraph(opts); } // $FlowFixMe[prop-missing] serialize(): SerializedContentGraph { + // $FlowFixMe[prop-missing] return { ...super.serialize(), _contentKeyToNodeId: this._contentKeyToNodeId, diff --git a/packages/core/graph/src/Graph.js b/packages/core/graph/src/Graph.js index d6fc0879fe7..77f15341165 100644 --- a/packages/core/graph/src/Graph.js +++ b/packages/core/graph/src/Graph.js @@ -1,49 +1,42 @@ // @flow strict-local -import {toNodeId, fromNodeId} from './types'; +import {fromNodeId} from './types'; +import AdjacencyList, {type SerializedAdjacencyList} from './AdjacencyList'; import type {Edge, NodeId} from './types'; import type {TraversalActions, GraphVisitor} from '@parcel/types'; import assert from 'assert'; import nullthrows from 'nullthrows'; -type NullEdgeType = 1; +export type NullEdgeType = 1; export type GraphOpts = {| nodes?: Map, - edges?: AdjacencyListMap, + adjacencyList?: SerializedAdjacencyList, rootNodeId?: ?NodeId, - nextNodeId?: ?number, |}; -export const ALL_EDGE_TYPES = '@@all_edge_types'; +export type SerializedGraph = {| + nodes: Map, + adjacencyList: SerializedAdjacencyList, + rootNodeId: ?NodeId, +|}; + +export type AllEdgeTypes = -1; +export const ALL_EDGE_TYPES: AllEdgeTypes = -1; export default class Graph { nodes: Map; - inboundEdges: AdjacencyList; - outboundEdges: AdjacencyList; + adjacencyList: AdjacencyList; rootNodeId: ?NodeId; - nextNodeId: number = 1; constructor(opts: ?GraphOpts) { this.nodes = opts?.nodes || new Map(); this.setRootNodeId(opts?.rootNodeId); - this.nextNodeId = opts?.nextNodeId ?? 0; - - let edges = opts?.edges; - if (edges != null) { - this.inboundEdges = new AdjacencyList(); - this.outboundEdges = new AdjacencyList(edges); - for (let [from, edgeList] of edges) { - for (let [type, toNodes] of edgeList) { - for (let to of toNodes) { - this.inboundEdges.addEdge(to, from, type); - } - } - } - } else { - this.inboundEdges = new AdjacencyList(); - this.outboundEdges = new AdjacencyList(); - } + + let adjacencyList = opts?.adjacencyList; + this.adjacencyList = adjacencyList + ? AdjacencyList.deserialize(adjacencyList) + : new AdjacencyList(); } setRootNodeId(id: ?NodeId) { @@ -55,37 +48,27 @@ export default class Graph { ): Graph { return new this({ nodes: opts.nodes, - edges: opts.edges, + adjacencyList: opts.adjacencyList, rootNodeId: opts.rootNodeId, - nextNodeId: opts.nextNodeId, }); } - serialize(): GraphOpts { + serialize(): SerializedGraph { return { nodes: this.nodes, - edges: this.outboundEdges.getListMap(), + adjacencyList: this.adjacencyList.serialize(), rootNodeId: this.rootNodeId, - nextNodeId: this.nextNodeId, }; } - // Returns a list of all edges in the graph. This can be large, so iterating + // Returns an iterator of all edges in the graph. This can be large, so iterating // the complete list can be costly in large graphs. Used when merging graphs. - getAllEdges(): Array> { - let edges = []; - for (let [from, edgeList] of this.outboundEdges.getListMap()) { - for (let [type, toNodes] of edgeList) { - for (let to of toNodes) { - edges.push({from, to, type}); - } - } - } - return edges; + getAllEdges(): Iterator> { + return this.adjacencyList.getAllEdges(); } addNode(node: TNode): NodeId { - let id = toNodeId(this.nextNodeId++); + let id = this.adjacencyList.addNode(); this.nodes.set(id, node); return id; } @@ -98,7 +81,15 @@ export default class Graph { return this.nodes.get(id); } - addEdge(from: NodeId, to: NodeId, type: TEdgeType | NullEdgeType = 1): void { + addEdge( + from: NodeId, + to: NodeId, + type: TEdgeType | NullEdgeType = 1, + ): boolean { + if (Number(type) === 0) { + throw new Error(`Edge type "${type}" not allowed`); + } + if (!this.getNode(from)) { throw new Error(`"from" node '${fromNodeId(from)}' not found`); } @@ -107,8 +98,7 @@ export default class Graph { throw new Error(`"to" node '${fromNodeId(to)}' not found`); } - this.outboundEdges.addEdge(from, to, type); - this.inboundEdges.addEdge(to, from, type); + return this.adjacencyList.addEdge(from, to, type); } hasEdge( @@ -116,95 +106,52 @@ export default class Graph { to: NodeId, type?: TEdgeType | NullEdgeType = 1, ): boolean { - return this.outboundEdges.hasEdge(from, to, type); + return this.adjacencyList.hasEdge(from, to, type); } getNodeIdsConnectedTo( nodeId: NodeId, - type: TEdgeType | NullEdgeType | Array = 1, + type: + | TEdgeType + | NullEdgeType + | Array + | AllEdgeTypes = 1, ): Array { this._assertHasNodeId(nodeId); - let inboundByType = this.inboundEdges.getEdgesByType(nodeId); - if (inboundByType == null) { - return []; - } - - let nodes; - if (type === ALL_EDGE_TYPES) { - nodes = new Set(); - for (let [, typeNodes] of inboundByType) { - for (let node of typeNodes) { - nodes.add(node); - } - } - } else if (Array.isArray(type)) { - nodes = new Set(); - for (let typeName of type) { - for (let node of inboundByType.get(typeName)?.values() ?? []) { - nodes.add(node); - } - } - } else { - nodes = new Set(inboundByType.get(type)?.values() ?? []); - } - - return [...nodes]; + return this.adjacencyList.getNodeIdsConnectedTo(nodeId, type); } getNodeIdsConnectedFrom( nodeId: NodeId, - type: TEdgeType | NullEdgeType | Array = 1, + type: + | TEdgeType + | NullEdgeType + | Array + | AllEdgeTypes = 1, ): Array { this._assertHasNodeId(nodeId); - let outboundByType = this.outboundEdges.getEdgesByType(nodeId); - if (outboundByType == null) { - return []; - } - let nodes; - if (type === ALL_EDGE_TYPES) { - nodes = new Set(); - for (let [, typeNodes] of outboundByType) { - for (let node of typeNodes) { - nodes.add(node); - } - } - } else if (Array.isArray(type)) { - nodes = new Set(); - for (let typeName of type) { - for (let node of outboundByType.get(typeName)?.values() ?? []) { - nodes.add(node); - } - } - } else { - nodes = new Set(outboundByType.get(type)?.values() ?? []); - } - - return [...nodes]; + return this.adjacencyList.getNodeIdsConnectedFrom(nodeId, type); } // Removes node and any edges coming from or to that node removeNode(nodeId: NodeId) { this._assertHasNodeId(nodeId); - for (let [type, nodesForType] of this.inboundEdges.getEdgesByType(nodeId)) { - for (let from of nodesForType) { - this.removeEdge( - from, - nodeId, - type, - // Do not allow orphans to be removed as this node could be one - // and is already being removed. - false /* removeOrphans */, - ); - } + for (let {type, from} of this.adjacencyList.getInboundEdgesByType(nodeId)) { + this.removeEdge( + from, + nodeId, + type, + // Do not allow orphans to be removed as this node could be one + // and is already being removed. + false, + ); } - for (let [type, toNodes] of this.outboundEdges.getEdgesByType(nodeId)) { - for (let to of toNodes) { - this.removeEdge(nodeId, to, type); - } + for (let {type, to} of this.adjacencyList.getOutboundEdgesByType(nodeId)) { + this.removeEdge(nodeId, to, type); } let wasRemoved = this.nodes.delete(nodeId); @@ -214,7 +161,7 @@ export default class Graph { removeEdges(nodeId: NodeId, type: TEdgeType | NullEdgeType = 1) { this._assertHasNodeId(nodeId); - for (let to of this.outboundEdges.getEdges(nodeId, type)) { + for (let to of this.getNodeIdsConnectedFrom(nodeId, type)) { this.removeEdge(nodeId, to, type); } } @@ -226,23 +173,13 @@ export default class Graph { type: TEdgeType | NullEdgeType = 1, removeOrphans: boolean = true, ) { - if (!this.outboundEdges.hasEdge(from, to, type)) { + if (!this.adjacencyList.hasEdge(from, to, type)) { throw new Error( - `Outbound edge from ${fromNodeId(from)} to ${fromNodeId( - to, - )} not found!`, + `Edge from ${fromNodeId(from)} to ${fromNodeId(to)} not found!`, ); } - if (!this.inboundEdges.hasEdge(to, from, type)) { - throw new Error( - `Inbound edge from ${fromNodeId(to)} to ${fromNodeId(from)} not found!`, - ); - } - - this.outboundEdges.removeEdge(from, to, type); - this.inboundEdges.removeEdge(to, from, type); - + this.adjacencyList.removeEdge(from, to, type); if (removeOrphans && this.isOrphanedNode(to)) { this.removeNode(to); } @@ -254,14 +191,7 @@ export default class Graph { if (this.rootNodeId == null) { // If the graph does not have a root, and there are inbound edges, // this node should not be considered orphaned. - // return false; - for (let [, inboundNodeIds] of this.inboundEdges.getEdgesByType(nodeId)) { - if (inboundNodeIds.size > 0) { - return false; - } - } - - return true; + return !this.adjacencyList.hasInboundEdges(nodeId); } // Otherwise, attempt to traverse backwards to the root. If there is a path, @@ -276,7 +206,6 @@ export default class Graph { actions.stop(); } }, - // $FlowFixMe ALL_EDGE_TYPES, ); @@ -292,19 +221,6 @@ export default class Graph { this.nodes.set(nodeId, node); } - replaceNode( - fromNodeId: NodeId, - toNodeId: NodeId, - type: TEdgeType | NullEdgeType = 1, - ): void { - this._assertHasNodeId(fromNodeId); - for (let parent of this.inboundEdges.getEdges(fromNodeId, type)) { - this.addEdge(parent, toNodeId, type); - this.removeEdge(parent, fromNodeId, type); - } - this.removeNode(fromNodeId); - } - // Update a node's downstream nodes making sure to prune any orphaned branches replaceNodeIdsConnectedTo( fromNodeId: NodeId, @@ -314,10 +230,10 @@ export default class Graph { ): void { this._assertHasNodeId(fromNodeId); - let outboundEdges = this.outboundEdges.getEdges(fromNodeId, type); + let outboundEdges = this.getNodeIdsConnectedFrom(fromNodeId, type); let childrenToRemove = new Set( replaceFilter - ? [...outboundEdges].filter(toNodeId => replaceFilter(toNodeId)) + ? outboundEdges.filter(toNodeId => replaceFilter(toNodeId)) : outboundEdges, ); for (let toNodeId of toNodeIds) { @@ -336,7 +252,11 @@ export default class Graph { traverse( visit: GraphVisitor, startNodeId: ?NodeId, - type: TEdgeType | NullEdgeType | Array = 1, + type: + | TEdgeType + | NullEdgeType + | Array + | AllEdgeTypes = 1, ): ?TContext { return this.dfs({ visit, @@ -349,7 +269,7 @@ export default class Graph { filter: (NodeId, TraversalActions) => ?TValue, visit: GraphVisitor, startNodeId: ?NodeId, - type?: TEdgeType | Array, + type?: TEdgeType | Array | AllEdgeTypes, ): ?TContext { return this.traverse(mapVisitor(filter, visit), startNodeId, type); } @@ -357,7 +277,11 @@ export default class Graph { traverseAncestors( startNodeId: ?NodeId, visit: GraphVisitor, - type: TEdgeType | NullEdgeType | Array = 1, + type: + | TEdgeType + | NullEdgeType + | Array + | AllEdgeTypes = 1, ): ?TContext { return this.dfs({ visit, @@ -574,47 +498,3 @@ export function mapVisitor( return mapped; } - -type AdjacencyListMap = Map>>; -class AdjacencyList { - _listMap: AdjacencyListMap; - - constructor(listMap?: AdjacencyListMap) { - this._listMap = listMap ?? new Map(); - } - - getListMap(): AdjacencyListMap { - return this._listMap; - } - - getEdges(from: NodeId, type: TEdgeType): $ReadOnlySet { - return this._listMap.get(from)?.get(type) ?? new Set(); - } - - getEdgesByType(from: NodeId): $ReadOnlyMap> { - return this._listMap.get(from) ?? new Map(); - } - - hasEdge(from: NodeId, to: NodeId, type: TEdgeType): boolean { - return Boolean(this._listMap.get(from)?.get(type)?.has(to)); - } - - addEdge(from: NodeId, to: NodeId, type: TEdgeType): void { - let types = this._listMap.get(from); - if (types == null) { - types = new Map>(); - this._listMap.set(from, types); - } - - let adjacent = types.get(type); - if (adjacent == null) { - adjacent = new Set(); - types.set(type, adjacent); - } - adjacent.add(to); - } - - removeEdge(from: NodeId, to: NodeId, type: TEdgeType): void { - this._listMap.get(from)?.get(type)?.delete(to); - } -} diff --git a/packages/core/graph/src/index.js b/packages/core/graph/src/index.js index a364b030efe..0eaafecc4ed 100644 --- a/packages/core/graph/src/index.js +++ b/packages/core/graph/src/index.js @@ -1,6 +1,8 @@ // @flow strict-local export type {NodeId, ContentKey, Edge} from './types'; +export type {GraphOpts} from './Graph'; +export type {ContentGraphOpts, SerializedContentGraph} from './ContentGraph'; export {toNodeId, fromNodeId} from './types'; -export {default as Graph, ALL_EDGE_TYPES, GraphOpts, mapVisitor} from './Graph'; -export {default as ContentGraph, SerializedContentGraph} from './ContentGraph'; +export {default as Graph, ALL_EDGE_TYPES, mapVisitor} from './Graph'; +export {default as ContentGraph} from './ContentGraph'; diff --git a/packages/core/graph/test/AdjacencyList.test.js b/packages/core/graph/test/AdjacencyList.test.js new file mode 100644 index 00000000000..4814422083e --- /dev/null +++ b/packages/core/graph/test/AdjacencyList.test.js @@ -0,0 +1,280 @@ +// @flow strict-local + +import assert from 'assert'; +import path from 'path'; +import {Worker} from 'worker_threads'; + +import AdjacencyList, {NodeTypeMap, EdgeTypeMap} from '../src/AdjacencyList'; +import {toNodeId} from '../src/types'; + +describe('AdjacencyList', () => { + it('constructor should initialize an empty graph', () => { + let stats = new AdjacencyList().stats; + assert(stats.nodes === 0); + assert(stats.edges === 0); + }); + + it('addNode should add a node to the graph', () => { + let graph = new AdjacencyList(); + let id = graph.addNode(); + assert.equal(id, 0); + assert.equal(graph.stats.nodes, 1); + }); + + it('addNode should resize nodes array when necessary', () => { + let graph = new AdjacencyList(); + let size = graph.serialize().nodes.byteLength; + let a = graph.addNode(); + let b = graph.addNode(); + assert(size < (size = graph.serialize().nodes.byteLength)); + graph.addEdge(a, b, 1); + graph.addEdge(a, b, 2); + graph.addEdge(a, b, 3); + graph.addEdge(a, b, 4); + assert(size < graph.serialize().nodes.byteLength); + }); + + it('removeEdge should remove an edge from the graph', () => { + let graph = new AdjacencyList(); + let node0 = graph.addNode(); + let node1 = graph.addNode(); + let node2 = graph.addNode(); + let node3 = graph.addNode(); + let node4 = graph.addNode(); + let node5 = graph.addNode(); + let node6 = graph.addNode(); + graph.addEdge(node0, node1); + graph.addEdge(node2, node1); + // this will get removed + graph.addEdge(node3, node1); + graph.addEdge(node4, node1); + graph.addEdge(node5, node1); + graph.addEdge(node6, node1); + + assert.deepEqual(graph.getNodeIdsConnectedTo(node1), [0, 2, 3, 4, 5, 6]); + + graph.removeEdge(node3, node1); + assert.deepEqual(graph.getNodeIdsConnectedTo(node1), [0, 2, 4, 5, 6]); + }); + + it('removeEdge should remove an edge of a specific type from the graph', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + let c = graph.addNode(); + let d = graph.addNode(); + graph.addEdge(a, b); + graph.addEdge(a, b, 2); + graph.addEdge(a, b, 3); + graph.addEdge(a, c); + graph.addEdge(a, d, 3); + assert.equal(graph.stats.edges, 5); + assert.ok(graph.hasEdge(a, b)); + assert.ok(graph.hasEdge(a, b, 2)); + assert.ok(graph.hasEdge(a, b, 3)); + assert.ok(graph.hasEdge(a, c)); + assert.ok(graph.hasEdge(a, d, 3)); + assert.deepEqual(Array.from(graph.getAllEdges()), [ + {from: a, to: b, type: 1}, + {from: a, to: b, type: 2}, + {from: a, to: b, type: 3}, + {from: a, to: c, type: 1}, + {from: a, to: d, type: 3}, + ]); + + graph.removeEdge(a, b, 2); + assert.equal(graph.stats.edges, 4); + assert.ok(graph.hasEdge(a, b)); + assert.equal(graph.hasEdge(a, b, 2), false); + assert.ok(graph.hasEdge(a, b, 3)); + assert.ok(graph.hasEdge(a, c)); + assert.ok(graph.hasEdge(a, d, 3)); + assert.deepEqual(Array.from(graph.getAllEdges()), [ + {from: a, to: b, type: 1}, + {from: a, to: b, type: 3}, + {from: a, to: c, type: 1}, + {from: a, to: d, type: 3}, + ]); + }); + + it('addEdge should add an edge to the graph', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + graph.addEdge(a, b); + assert.equal(graph.stats.nodes, 2); + assert.equal(graph.stats.edges, 1); + assert.ok(graph.hasEdge(a, b)); + }); + + it('addEdge should add multiple edges from a node in order', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + let c = graph.addNode(); + let d = graph.addNode(); + graph.addEdge(a, b); + graph.addEdge(a, d); + graph.addEdge(a, c); + assert.deepEqual(graph.getNodeIdsConnectedFrom(a), [b, d, c]); + }); + + it('addEdge should add multiple edges to a node in order', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + let c = graph.addNode(); + let d = graph.addNode(); + graph.addEdge(a, b); + graph.addEdge(d, b); + graph.addEdge(a, d); + graph.addEdge(c, b); + assert.deepEqual(graph.getNodeIdsConnectedTo(b), [a, d, c]); + }); + + it('addEdge should add multiple edges of different types in order', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + graph.addEdge(a, b); + graph.addEdge(a, b, 1); + graph.addEdge(a, b, 4); + graph.addEdge(a, b, 3); + assert.deepEqual(graph.getNodeIdsConnectedFrom(a), [b]); + assert.deepEqual(Array.from(graph.getAllEdges()), [ + {from: a, to: b, type: 1}, + {from: a, to: b, type: 4}, + {from: a, to: b, type: 3}, + ]); + }); + + it('addEdge should return false if an edge is already added', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + assert.equal(graph.addEdge(a, b), true); + assert.equal(graph.addEdge(a, b), false); + }); + + it('addEdge should resize edges array when necessary', () => { + let graph = new AdjacencyList(); + let size = graph.serialize().edges.byteLength; + let a = graph.addNode(); + let b = graph.addNode(); + graph.addEdge(a, b, 1); + graph.addEdge(a, b, 2); + graph.addEdge(a, b, 3); + assert(size < graph.serialize().edges.byteLength); + }); + + it('addEdge should error when a node has not been added to the graph', () => { + let graph = new AdjacencyList(); + assert.throws(() => graph.addEdge(toNodeId(0), toNodeId(1))); + graph.addNode(); + assert.throws(() => graph.addEdge(toNodeId(0), toNodeId(1))); + graph.addNode(); + assert.doesNotThrow(() => graph.addEdge(toNodeId(0), toNodeId(1))); + assert.throws(() => graph.addEdge(toNodeId(0), toNodeId(2))); + }); + + it('addEdge should error when an unsupported edge type is provided', () => { + let graph = new AdjacencyList(); + let a = graph.addNode(); + let b = graph.addNode(); + assert.throws(() => graph.addEdge(a, b, 0)); + assert.throws(() => graph.addEdge(a, b, -1)); + assert.doesNotThrow(() => graph.addEdge(a, b, 1)); + }); + + it('addEdge should not replace a deleted edge if the edge was already added', () => { + // Mock hash fn to generate collisions + // $FlowFixMe[prop-missing] + let originalHash = AdjacencyList.prototype.hash; + // $FlowFixMe[prop-missing] + AdjacencyList.prototype.hash = () => 1; + + let graph = new AdjacencyList(); + let n0 = graph.addNode(); + let n1 = graph.addNode(); + let n2 = graph.addNode(); + graph.addEdge(n0, n1, 1); + graph.addEdge(n1, n2, 1); + graph.removeEdge(n1, n2, 1); + assert(graph.addEdge(n0, n1, 1) === false); + assert(graph.stats.edges === 1); + + // $FlowFixMe[prop-missing] + AdjacencyList.prototype.hash = originalHash; + }); + + it('addEdge should replace a deleted edge', () => { + // Mock hash fn to generate collisions + // $FlowFixMe[prop-missing] + let originalHash = AdjacencyList.prototype.hash; + // $FlowFixMe[prop-missing] + AdjacencyList.prototype.hash = () => 1; + + let graph = new AdjacencyList(); + let n0 = graph.addNode(); + let n1 = graph.addNode(); + graph.addEdge(n0, n1, 2); + graph.removeEdge(n0, n1, 2); + assert(graph.addEdge(n0, n1, 2)); + assert(graph.stats.edges === 1); + assert(graph.stats.deleted === 1); + // Resize to reclaim deleted edge space. + graph.resizeEdges(4); + assert(graph.stats.edges === 1); + assert(graph.stats.deleted === 0); + + // $FlowFixMe[prop-missing] + AdjacencyList.prototype.hash = originalHash; + }); + + describe('deserialize', function () { + this.timeout(10000); + + it('should share the underlying data across worker threads', async () => { + let graph = new AdjacencyList(); + let n0 = graph.addNode(); + let n1 = graph.addNode(); + graph.addEdge(n0, n1, 1); + graph.addEdge(n0, n1, 2); + + let worker = new Worker( + path.join(__dirname, 'integration/adjacency-list-shared-array.js'), + ); + + let originalSerialized = graph.serialize(); + let originalNodes = [...originalSerialized.nodes]; + let originalEdges = [...originalSerialized.edges]; + let work = new Promise(resolve => worker.on('message', resolve)); + worker.postMessage(originalSerialized); + let received = AdjacencyList.deserialize(await work); + await worker.terminate(); + + assert.deepEqual(received.serialize().nodes, graph.serialize().nodes); + assert.deepEqual(received.serialize().edges, graph.serialize().edges); + + originalNodes.forEach((v, i) => { + if (i < NodeTypeMap.HEADER_SIZE) { + assert.equal(v, received.serialize().nodes[i]); + assert.equal(v, graph.serialize().nodes[i]); + } else { + assert.equal(v * 2, received.serialize().nodes[i]); + assert.equal(v * 2, graph.serialize().nodes[i]); + } + }); + + originalEdges.forEach((v, i) => { + if (i < EdgeTypeMap.HEADER_SIZE) { + assert.equal(v, received.serialize().edges[i]); + assert.equal(v, graph.serialize().edges[i]); + } else { + assert.equal(v * 2, received.serialize().edges[i]); + assert.equal(v * 2, graph.serialize().edges[i]); + } + }); + }); + }); +}); diff --git a/packages/core/graph/test/Graph.test.js b/packages/core/graph/test/Graph.test.js index bbecc3a28ec..fb0849966da 100644 --- a/packages/core/graph/test/Graph.test.js +++ b/packages/core/graph/test/Graph.test.js @@ -10,7 +10,7 @@ describe('Graph', () => { it('constructor should initialize an empty graph', () => { let graph = new Graph(); assert.deepEqual(graph.nodes, new Map()); - assert.deepEqual(graph.getAllEdges(), []); + assert.deepEqual([...graph.getAllEdges()], []); }); it('addNode should add a node to the graph', () => { @@ -114,7 +114,10 @@ describe('Graph', () => { assert(graph.nodes.has(nodeD)); assert(!graph.nodes.has(nodeB)); assert(!graph.nodes.has(nodeC)); - assert.deepEqual(graph.getAllEdges(), [{from: nodeA, to: nodeD, type: 1}]); + assert.deepEqual( + [...graph.getAllEdges()], + [{from: nodeA, to: nodeD, type: 1}], + ); }); it('removing a node recursively deletes orphaned nodes', () => { @@ -154,7 +157,7 @@ describe('Graph', () => { graph.removeNode(nodeB); assert.deepEqual([...graph.nodes.keys()], [nodeA, nodeC, nodeF]); - assert.deepEqual(graph.getAllEdges(), [ + assert.deepEqual(Array.from(graph.getAllEdges()), [ {from: nodeA, to: nodeC, type: 1}, {from: nodeC, to: nodeF, type: 1}, ]); @@ -199,7 +202,7 @@ describe('Graph', () => { graph.removeNode(nodeB); assert.deepEqual([...graph.nodes.keys()], [nodeA, nodeC, nodeF]); - assert.deepEqual(graph.getAllEdges(), [ + assert.deepEqual(Array.from(graph.getAllEdges()), [ {from: nodeA, to: nodeC, type: 1}, {from: nodeC, to: nodeF, type: 1}, ]); @@ -234,7 +237,7 @@ describe('Graph', () => { graph.removeEdge(nodeC, nodeE); assert.deepEqual(nodesBefore, getNodeIds()); - assert.deepEqual(graph.getAllEdges(), [ + assert.deepEqual(Array.from(graph.getAllEdges()), [ {from: nodeA, to: nodeB, type: 1}, {from: nodeB, to: nodeC, type: 1}, {from: nodeB, to: nodeD, type: 1}, @@ -277,7 +280,7 @@ describe('Graph', () => { assert(graph.hasNode(nodeB)); assert(!graph.hasNode(nodeC)); assert(graph.hasNode(nodeD)); - assert.deepEqual(graph.getAllEdges(), [ + assert.deepEqual(Array.from(graph.getAllEdges()), [ {from: nodeA, to: nodeB, type: 1}, {from: nodeA, to: nodeD, type: 1}, ]); diff --git a/packages/core/graph/test/integration/adjacency-list-shared-array.js b/packages/core/graph/test/integration/adjacency-list-shared-array.js new file mode 100644 index 00000000000..0c3460f92a8 --- /dev/null +++ b/packages/core/graph/test/integration/adjacency-list-shared-array.js @@ -0,0 +1,20 @@ +require('@parcel/babel-register'); +const {parentPort} = require('worker_threads'); +const { + default: AdjacencyList, + NodeTypeMap, + EdgeTypeMap, +} = require('../../src/AdjacencyList'); + +parentPort.once('message', (serialized) => { + let graph = AdjacencyList.deserialize(serialized); + serialized.nodes.forEach((v, i) => { + if (i < NodeTypeMap.HEADER_SIZE) return; + serialized.nodes[i] = v * 2; + }); + serialized.edges.forEach((v, i) => { + if (i < EdgeTypeMap.HEADER_SIZE) return; + serialized.edges[i] = v * 2; + }); + parentPort.postMessage(graph.serialize()); +}); diff --git a/packages/core/types/index.js b/packages/core/types/index.js index fb7fd5e6efb..a30e6fca871 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -186,7 +186,7 @@ export type EnvironmentOptions = {| */ export type VersionMap = { [string]: string, - ..., + ... }; export type EnvironmentFeature = @@ -398,9 +398,11 @@ export interface AssetSymbols // eslint-disable-next-line no-undef * This is the default state. */ +isCleared: boolean; - get( - exportSymbol: Symbol, - ): ?{|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}; + get(exportSymbol: Symbol): ?{| + local: Symbol, + loc: ?SourceLocation, + meta?: ?Meta, + |}; hasExportSymbol(exportSymbol: Symbol): boolean; hasLocalSymbol(local: Symbol): boolean; exportSymbols(): Iterable; @@ -439,9 +441,12 @@ export interface MutableDependencySymbols // eslint-disable-next-line no-undef * This is the default state. */ +isCleared: boolean; - get( - exportSymbol: Symbol, - ): ?{|local: Symbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|}; + get(exportSymbol: Symbol): ?{| + local: Symbol, + loc: ?SourceLocation, + isWeak: boolean, + meta?: ?Meta, + |}; hasExportSymbol(exportSymbol: Symbol): boolean; hasLocalSymbol(local: Symbol): boolean; exportSymbols(): Iterable; diff --git a/packages/transformers/js/native.js b/packages/transformers/js/native.js index ad423318745..ce1d82238c9 100644 --- a/packages/transformers/js/native.js +++ b/packages/transformers/js/native.js @@ -18,7 +18,7 @@ if (process.env.PARCEL_BUILD_ENV === 'production') { } else if (process.env.PARCEL_SWC_WASM) { const {transform} = require('./wasm/dist-node/parcel_js_swc_wasm.js'); - module.exports.transform = function(config) { + module.exports.transform = function (config) { let result = transform(config); return { ...result,