@parcel/graph 2.0.2-nightly.2536 → 2.0.2-nightly.2549
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AdjacencyList.js +1165 -0
- package/lib/ContentGraph.js +1 -0
- package/lib/Graph.js +40 -213
- package/lib/index.js +3 -13
- package/package.json +2 -2
- package/src/AdjacencyList.js +1211 -0
- package/src/ContentGraph.js +9 -4
- package/src/Graph.js +77 -197
- package/src/index.js +4 -2
- package/test/AdjacencyList.test.js +280 -0
- package/test/Graph.test.js +9 -6
- package/test/integration/adjacency-list-shared-array.js +20 -0
@@ -0,0 +1,1211 @@
|
|
1
|
+
// @flow
|
2
|
+
import assert from 'assert';
|
3
|
+
import nullthrows from 'nullthrows';
|
4
|
+
import {fromNodeId, toNodeId} from './types';
|
5
|
+
import {ALL_EDGE_TYPES, type NullEdgeType, type AllEdgeTypes} from './Graph';
|
6
|
+
import type {NodeId} from './types';
|
7
|
+
|
8
|
+
/** The address of the node in the nodes map. */
|
9
|
+
opaque type NodeAddress = number;
|
10
|
+
|
11
|
+
opaque type EdgeHash = number;
|
12
|
+
|
13
|
+
/** The address of the edge in the edges map. */
|
14
|
+
opaque type EdgeAddress = number;
|
15
|
+
|
16
|
+
// eslint-disable-next-line no-unused-vars
|
17
|
+
export type SerializedAdjacencyList<TEdgeType> = {|
|
18
|
+
nodes: Uint32Array,
|
19
|
+
edges: Uint32Array,
|
20
|
+
|};
|
21
|
+
|
22
|
+
// eslint-disable-next-line no-unused-vars
|
23
|
+
export type AdjacencyListOptions<TEdgeType> = {|
|
24
|
+
edgeCapacity?: number,
|
25
|
+
nodeCapacity?: number,
|
26
|
+
|};
|
27
|
+
|
28
|
+
/** The upper bound above which capacity should be increased. */
|
29
|
+
const LOAD_FACTOR = 0.7;
|
30
|
+
/** The lower bound below which capacity should be decreased. */
|
31
|
+
const UNLOAD_FACTOR = 0.3;
|
32
|
+
/** The max amount by which to grow the capacity. */
|
33
|
+
const MAX_GROW_FACTOR = 8;
|
34
|
+
/** The min amount by which to grow the capacity. */
|
35
|
+
const MIN_GROW_FACTOR = 2;
|
36
|
+
/** The amount by which to shrink the capacity. */
|
37
|
+
const SHRINK_FACTOR = 0.5;
|
38
|
+
|
39
|
+
export default class AdjacencyList<TEdgeType: number = 1> {
|
40
|
+
#nodes /*: NodeTypeMap<TEdgeType | NullEdgeType> */;
|
41
|
+
#edges /*: EdgeTypeMap<TEdgeType | NullEdgeType> */;
|
42
|
+
|
43
|
+
constructor(
|
44
|
+
opts?:
|
45
|
+
| SerializedAdjacencyList<TEdgeType | NullEdgeType>
|
46
|
+
| AdjacencyListOptions<TEdgeType | NullEdgeType>,
|
47
|
+
) {
|
48
|
+
let nodes;
|
49
|
+
let edges;
|
50
|
+
|
51
|
+
if (opts?.nodes) {
|
52
|
+
({nodes, edges} = opts);
|
53
|
+
this.#nodes = new NodeTypeMap(nodes);
|
54
|
+
this.#edges = new EdgeTypeMap(edges);
|
55
|
+
} else {
|
56
|
+
let {
|
57
|
+
nodeCapacity = NodeTypeMap.MIN_CAPACITY,
|
58
|
+
edgeCapacity = EdgeTypeMap.MIN_CAPACITY,
|
59
|
+
} = opts ?? {};
|
60
|
+
assert(
|
61
|
+
nodeCapacity <= NodeTypeMap.MAX_CAPACITY,
|
62
|
+
'Node capacity overflow!',
|
63
|
+
);
|
64
|
+
assert(
|
65
|
+
edgeCapacity <= EdgeTypeMap.MAX_CAPACITY,
|
66
|
+
'Edge capacity overflow!',
|
67
|
+
);
|
68
|
+
this.#nodes = new NodeTypeMap(nodeCapacity);
|
69
|
+
this.#edges = new EdgeTypeMap(edgeCapacity);
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Create a new `AdjacencyList` from the given options.
|
75
|
+
*/
|
76
|
+
static deserialize(
|
77
|
+
opts: SerializedAdjacencyList<TEdgeType>,
|
78
|
+
): AdjacencyList<TEdgeType> {
|
79
|
+
return new AdjacencyList(opts);
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Returns a serializable object of the nodes and edges in the graph.
|
84
|
+
*/
|
85
|
+
serialize(): SerializedAdjacencyList<TEdgeType> {
|
86
|
+
return {
|
87
|
+
nodes: this.#nodes.data,
|
88
|
+
edges: this.#edges.data,
|
89
|
+
};
|
90
|
+
}
|
91
|
+
|
92
|
+
get stats(): {|
|
93
|
+
/** The number of nodes in the graph. */
|
94
|
+
nodes: number,
|
95
|
+
/** The number of edge types associated with nodes in the graph. */
|
96
|
+
nodeEdgeTypes: number,
|
97
|
+
/** The maximum number of nodes the graph can contain. */
|
98
|
+
nodeCapacity: number,
|
99
|
+
/** The size of the raw nodes buffer, in mb. */
|
100
|
+
nodeBufferSize: string,
|
101
|
+
/** The current load on the nodes array. */
|
102
|
+
nodeLoad: string,
|
103
|
+
/** The number of edges in the graph. */
|
104
|
+
edges: number,
|
105
|
+
/** The number of edges deleted from the graph. */
|
106
|
+
deleted: number,
|
107
|
+
/** The maximum number of edges the graph can contain. */
|
108
|
+
edgeCapacity: number,
|
109
|
+
/** The size of the raw edges buffer, in mb. */
|
110
|
+
edgeBufferSize: string,
|
111
|
+
/** The current load on the edges array, including deletes. */
|
112
|
+
edgeLoadWithDeletes: string,
|
113
|
+
/** The current load on the edges array. */
|
114
|
+
edgeLoad: string,
|
115
|
+
/** The total number of edge hash collisions. */
|
116
|
+
collisions: number,
|
117
|
+
/** The number of collisions for the most common hash. */
|
118
|
+
maxCollisions: number,
|
119
|
+
/** The average number of collisions per hash. */
|
120
|
+
avgCollisions: number,
|
121
|
+
/** The likelihood of uniform distribution. ~1.0 indicates certainty. */
|
122
|
+
uniformity: number,
|
123
|
+
|} {
|
124
|
+
let buckets = new Map();
|
125
|
+
for (let {from, to, type} of this.getAllEdges()) {
|
126
|
+
let hash = this.#edges.hash(from, to, type);
|
127
|
+
let bucket = buckets.get(hash) || new Set();
|
128
|
+
let key = `${String(from)}, ${String(to)}, ${String(type)}`;
|
129
|
+
assert(!bucket.has(key), `Duplicate node detected: ${key}`);
|
130
|
+
bucket.add(key);
|
131
|
+
buckets.set(hash, bucket);
|
132
|
+
}
|
133
|
+
|
134
|
+
let maxCollisions = 0;
|
135
|
+
let collisions = 0;
|
136
|
+
let distribution = 0;
|
137
|
+
|
138
|
+
for (let bucket of buckets.values()) {
|
139
|
+
maxCollisions = Math.max(maxCollisions, bucket.size - 1);
|
140
|
+
collisions += bucket.size - 1;
|
141
|
+
distribution += (bucket.size * (bucket.size + 1)) / 2;
|
142
|
+
}
|
143
|
+
|
144
|
+
let uniformity =
|
145
|
+
distribution /
|
146
|
+
((this.#edges.count / (2 * this.#edges.capacity)) *
|
147
|
+
(this.#edges.count + 2 * this.#edges.capacity - 1));
|
148
|
+
|
149
|
+
return {
|
150
|
+
nodes: fromNodeId(this.#nodes.nextId),
|
151
|
+
nodeEdgeTypes: this.#nodes.count,
|
152
|
+
nodeCapacity: this.#nodes.capacity,
|
153
|
+
nodeLoad: `${Math.round(this.#nodes.load * 100)}%`,
|
154
|
+
nodeBufferSize: this.#nodes.bufferSize,
|
155
|
+
|
156
|
+
edges: this.#edges.count,
|
157
|
+
deleted: this.#edges.deletes,
|
158
|
+
edgeCapacity: this.#edges.capacity,
|
159
|
+
edgeLoad: `${Math.round(this.#edges.load * 100)}%`,
|
160
|
+
edgeLoadWithDeletes: `${Math.round(
|
161
|
+
this.#edges.getLoad(this.#edges.count + this.#edges.deletes) * 100,
|
162
|
+
)}%`,
|
163
|
+
edgeBufferSize: this.#edges.bufferSize,
|
164
|
+
|
165
|
+
collisions,
|
166
|
+
maxCollisions,
|
167
|
+
avgCollisions: Math.round((collisions / buckets.size) * 100) / 100 || 0,
|
168
|
+
uniformity: Math.round(uniformity * 100) / 100 || 0,
|
169
|
+
};
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Resize the internal nodes array.
|
174
|
+
*
|
175
|
+
* This is used in `addNode` when the `numNodes` meets or exceeds
|
176
|
+
* the allocated size of the `nodes` array.
|
177
|
+
*/
|
178
|
+
resizeNodes(size: number) {
|
179
|
+
let nodes = this.#nodes;
|
180
|
+
// Allocate the required space for a `nodes` map of the given `size`.
|
181
|
+
this.#nodes = new NodeTypeMap(size);
|
182
|
+
// Copy the existing nodes into the new array.
|
183
|
+
this.#nodes.set(nodes.data);
|
184
|
+
}
|
185
|
+
|
186
|
+
/**
|
187
|
+
* Resize the internal edges array.
|
188
|
+
*
|
189
|
+
* This is used in `addEdge` when the `numEdges` meets or exceeds
|
190
|
+
* the allocated size of the `edges` array.
|
191
|
+
*/
|
192
|
+
resizeEdges(size: number) {
|
193
|
+
// Allocate the required space for new `nodes` and `edges` maps.
|
194
|
+
let copy = new AdjacencyList({
|
195
|
+
nodeCapacity: this.#nodes.capacity,
|
196
|
+
edgeCapacity: size,
|
197
|
+
});
|
198
|
+
|
199
|
+
// Copy the existing edges into the new array.
|
200
|
+
copy.#nodes.nextId = this.#nodes.nextId;
|
201
|
+
this.#edges.forEach(
|
202
|
+
edge =>
|
203
|
+
void copy.addEdge(
|
204
|
+
this.#edges.from(edge),
|
205
|
+
this.#edges.to(edge),
|
206
|
+
this.#edges.typeOf(edge),
|
207
|
+
),
|
208
|
+
);
|
209
|
+
|
210
|
+
// We expect to preserve the same number of edges.
|
211
|
+
assert(
|
212
|
+
this.#edges.count === copy.#edges.count,
|
213
|
+
`Edge mismatch! ${this.#edges.count} does not match ${
|
214
|
+
copy.#edges.count
|
215
|
+
}.`,
|
216
|
+
);
|
217
|
+
|
218
|
+
// Finally, copy the new data arrays over to this graph.
|
219
|
+
this.#nodes = copy.#nodes;
|
220
|
+
this.#edges = copy.#edges;
|
221
|
+
}
|
222
|
+
|
223
|
+
/**
|
224
|
+
* Adds a node to the graph.
|
225
|
+
*
|
226
|
+
* Returns the id of the added node.
|
227
|
+
*/
|
228
|
+
addNode(): NodeId {
|
229
|
+
let id = this.#nodes.getId();
|
230
|
+
// If we're in danger of overflowing the `nodes` array, resize it.
|
231
|
+
if (this.#nodes.load > LOAD_FACTOR) {
|
232
|
+
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity));
|
233
|
+
}
|
234
|
+
return id;
|
235
|
+
}
|
236
|
+
|
237
|
+
/**
|
238
|
+
* Adds an edge to the graph.
|
239
|
+
*
|
240
|
+
* Returns `true` if the edge was added,
|
241
|
+
* or `false` if the edge already exists.
|
242
|
+
*/
|
243
|
+
addEdge(
|
244
|
+
from: NodeId,
|
245
|
+
to: NodeId,
|
246
|
+
type: TEdgeType | NullEdgeType = 1,
|
247
|
+
): boolean {
|
248
|
+
assert(type > 0, `Unsupported edge type ${0}`);
|
249
|
+
|
250
|
+
let hash = this.#edges.hash(from, to, type);
|
251
|
+
let edge = this.#edges.addressOf(hash, from, to, type);
|
252
|
+
|
253
|
+
// The edge is already in the graph; do nothing.
|
254
|
+
if (edge !== null) return false;
|
255
|
+
|
256
|
+
let capacity = this.#edges.capacity;
|
257
|
+
// We add 1 to account for the edge we are adding.
|
258
|
+
let count = this.#edges.count + 1;
|
259
|
+
// Since the space occupied by deleted edges isn't reclaimed,
|
260
|
+
// we include them in our count to avoid overflowing the `edges` array.
|
261
|
+
let deletes = this.#edges.deletes;
|
262
|
+
let total = count + deletes;
|
263
|
+
// If we have enough space to keep adding edges, we can
|
264
|
+
// put off reclaiming the deleted space until the next resize.
|
265
|
+
if (this.#edges.getLoad(total) > LOAD_FACTOR) {
|
266
|
+
if (this.#edges.getLoad(deletes) > UNLOAD_FACTOR) {
|
267
|
+
// If we have a significant number of deletes, we compute our new
|
268
|
+
// capacity based on the current count, even though we decided to
|
269
|
+
// resize based on the sum total of count and deletes.
|
270
|
+
// In this case, resizing is more like a compaction.
|
271
|
+
this.resizeEdges(
|
272
|
+
getNextEdgeCapacity(capacity, count, this.#edges.getLoad(count)),
|
273
|
+
);
|
274
|
+
} else {
|
275
|
+
this.resizeEdges(
|
276
|
+
getNextEdgeCapacity(capacity, total, this.#edges.getLoad(total)),
|
277
|
+
);
|
278
|
+
}
|
279
|
+
// We must rehash because the capacity has changed.
|
280
|
+
hash = this.#edges.hash(from, to, type);
|
281
|
+
}
|
282
|
+
|
283
|
+
let toNode = this.#nodes.addressOf(to, type);
|
284
|
+
let fromNode = this.#nodes.addressOf(from, type);
|
285
|
+
if (toNode === null || fromNode === null) {
|
286
|
+
// If we're in danger of overflowing the `nodes` array, resize it.
|
287
|
+
if (this.#nodes.load >= LOAD_FACTOR) {
|
288
|
+
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity));
|
289
|
+
// We need to update our indices since the `nodes` array has changed.
|
290
|
+
toNode = this.#nodes.addressOf(to, type);
|
291
|
+
fromNode = this.#nodes.addressOf(from, type);
|
292
|
+
}
|
293
|
+
}
|
294
|
+
if (toNode === null) toNode = this.#nodes.add(to, type);
|
295
|
+
if (fromNode === null) fromNode = this.#nodes.add(from, type);
|
296
|
+
|
297
|
+
// Add our new edge to its hash bucket.
|
298
|
+
edge = this.#edges.add(hash, from, to, type);
|
299
|
+
|
300
|
+
// Link this edge to the node's list of incoming edges.
|
301
|
+
let prevIn = this.#nodes.linkIn(toNode, edge);
|
302
|
+
if (prevIn !== null) this.#edges.linkIn(prevIn, edge);
|
303
|
+
|
304
|
+
// Link this edge to the node's list of outgoing edges.
|
305
|
+
let prevOut = this.#nodes.linkOut(fromNode, edge);
|
306
|
+
if (prevOut !== null) this.#edges.linkOut(prevOut, edge);
|
307
|
+
|
308
|
+
return true;
|
309
|
+
}
|
310
|
+
|
311
|
+
*getAllEdges(): Iterator<{|
|
312
|
+
type: TEdgeType | NullEdgeType,
|
313
|
+
from: NodeId,
|
314
|
+
to: NodeId,
|
315
|
+
|}> {
|
316
|
+
for (let edge of this.#edges) {
|
317
|
+
yield {
|
318
|
+
from: this.#edges.from(edge),
|
319
|
+
to: this.#edges.to(edge),
|
320
|
+
type: this.#edges.typeOf(edge),
|
321
|
+
};
|
322
|
+
}
|
323
|
+
}
|
324
|
+
|
325
|
+
/**
|
326
|
+
* Check if the graph has an edge connecting the `from` and `to` nodes.
|
327
|
+
*/
|
328
|
+
hasEdge(
|
329
|
+
from: NodeId,
|
330
|
+
to: NodeId,
|
331
|
+
type: TEdgeType | NullEdgeType = 1,
|
332
|
+
): boolean {
|
333
|
+
let hash = this.#edges.hash(from, to, type);
|
334
|
+
return this.#edges.addressOf(hash, from, to, type) !== null;
|
335
|
+
}
|
336
|
+
|
337
|
+
/**
|
338
|
+
*
|
339
|
+
*/
|
340
|
+
removeEdge(
|
341
|
+
from: NodeId,
|
342
|
+
to: NodeId,
|
343
|
+
type: TEdgeType | NullEdgeType = 1,
|
344
|
+
): void {
|
345
|
+
let hash = this.#edges.hash(from, to, type);
|
346
|
+
let edge = this.#edges.addressOf(hash, from, to, type);
|
347
|
+
|
348
|
+
// The edge is not in the graph; do nothing.
|
349
|
+
if (edge === null) return;
|
350
|
+
|
351
|
+
let toNode = nullthrows(this.#nodes.addressOf(to, type));
|
352
|
+
let fromNode = nullthrows(this.#nodes.addressOf(from, type));
|
353
|
+
|
354
|
+
// Update the terminating node's first and last incoming edges.
|
355
|
+
this.#nodes.unlinkIn(
|
356
|
+
toNode,
|
357
|
+
edge,
|
358
|
+
this.#edges.prevIn(edge),
|
359
|
+
this.#edges.nextIn(edge),
|
360
|
+
);
|
361
|
+
|
362
|
+
// Update the originating node's first and last outgoing edges.
|
363
|
+
this.#nodes.unlinkOut(
|
364
|
+
fromNode,
|
365
|
+
edge,
|
366
|
+
this.#edges.prevOut(edge),
|
367
|
+
this.#edges.nextOut(edge),
|
368
|
+
);
|
369
|
+
|
370
|
+
// Splice the removed edge out of the linked list of edges in the bucket.
|
371
|
+
this.#edges.unlink(hash, edge);
|
372
|
+
// Splice the removed edge out of the linked list of incoming edges.
|
373
|
+
this.#edges.unlinkIn(edge);
|
374
|
+
// Splice the removed edge out of the linked list of outgoing edges.
|
375
|
+
this.#edges.unlinkOut(edge);
|
376
|
+
// Finally, delete the edge.
|
377
|
+
this.#edges.delete(edge);
|
378
|
+
}
|
379
|
+
|
380
|
+
hasInboundEdges(to: NodeId): boolean {
|
381
|
+
let node = this.#nodes.head(to);
|
382
|
+
while (node !== null) {
|
383
|
+
if (this.#nodes.firstIn(node) !== null) return true;
|
384
|
+
node = this.#nodes.next(node);
|
385
|
+
}
|
386
|
+
return false;
|
387
|
+
}
|
388
|
+
|
389
|
+
getInboundEdgesByType(
|
390
|
+
to: NodeId,
|
391
|
+
): {|type: TEdgeType | NullEdgeType, from: NodeId|}[] {
|
392
|
+
let edges = [];
|
393
|
+
let node = this.#nodes.head(to);
|
394
|
+
while (node !== null) {
|
395
|
+
let type = this.#nodes.typeOf(node);
|
396
|
+
let edge = this.#nodes.firstIn(node);
|
397
|
+
while (edge !== null) {
|
398
|
+
let from = this.#edges.from(edge);
|
399
|
+
edges.push({from, type});
|
400
|
+
edge = this.#edges.nextIn(edge);
|
401
|
+
}
|
402
|
+
node = this.#nodes.next(node);
|
403
|
+
}
|
404
|
+
return edges;
|
405
|
+
}
|
406
|
+
|
407
|
+
getOutboundEdgesByType(
|
408
|
+
from: NodeId,
|
409
|
+
): {|type: TEdgeType | NullEdgeType, to: NodeId|}[] {
|
410
|
+
let edges = [];
|
411
|
+
let node = this.#nodes.head(from);
|
412
|
+
while (node !== null) {
|
413
|
+
let type = this.#nodes.typeOf(node);
|
414
|
+
let edge = this.#nodes.firstOut(node);
|
415
|
+
while (edge !== null) {
|
416
|
+
let to = this.#edges.to(edge);
|
417
|
+
edges.push({to, type});
|
418
|
+
edge = this.#edges.nextOut(edge);
|
419
|
+
}
|
420
|
+
node = this.#nodes.next(node);
|
421
|
+
}
|
422
|
+
return edges;
|
423
|
+
}
|
424
|
+
|
425
|
+
/**
|
426
|
+
* Get the list of nodes connected from this node.
|
427
|
+
*/
|
428
|
+
getNodeIdsConnectedFrom(
|
429
|
+
from: NodeId,
|
430
|
+
type:
|
431
|
+
| AllEdgeTypes
|
432
|
+
| TEdgeType
|
433
|
+
| NullEdgeType
|
434
|
+
| Array<TEdgeType | NullEdgeType> = 1,
|
435
|
+
): NodeId[] {
|
436
|
+
let matches = node =>
|
437
|
+
type === ALL_EDGE_TYPES ||
|
438
|
+
(Array.isArray(type)
|
439
|
+
? type.includes(this.#nodes.typeOf(node))
|
440
|
+
: type === this.#nodes.typeOf(node));
|
441
|
+
|
442
|
+
let nodes = [];
|
443
|
+
let node = this.#nodes.head(from);
|
444
|
+
while (node !== null) {
|
445
|
+
if (matches(node)) {
|
446
|
+
let edge = this.#nodes.firstOut(node);
|
447
|
+
while (edge !== null) {
|
448
|
+
nodes.push(this.#edges.to(edge));
|
449
|
+
edge = this.#edges.nextOut(edge);
|
450
|
+
}
|
451
|
+
}
|
452
|
+
node = this.#nodes.next(node);
|
453
|
+
}
|
454
|
+
return nodes;
|
455
|
+
}
|
456
|
+
|
457
|
+
/**
|
458
|
+
* Get the list of nodes connected to this node.
|
459
|
+
*/
|
460
|
+
getNodeIdsConnectedTo(
|
461
|
+
to: NodeId,
|
462
|
+
type:
|
463
|
+
| AllEdgeTypes
|
464
|
+
| TEdgeType
|
465
|
+
| NullEdgeType
|
466
|
+
| Array<TEdgeType | NullEdgeType> = 1,
|
467
|
+
): NodeId[] {
|
468
|
+
let matches = node =>
|
469
|
+
type === ALL_EDGE_TYPES ||
|
470
|
+
(Array.isArray(type)
|
471
|
+
? type.includes(this.#nodes.typeOf(node))
|
472
|
+
: type === this.#nodes.typeOf(node));
|
473
|
+
|
474
|
+
let nodes = [];
|
475
|
+
let node = this.#nodes.head(to);
|
476
|
+
while (node !== null) {
|
477
|
+
if (matches(node)) {
|
478
|
+
let edge = this.#nodes.firstIn(node);
|
479
|
+
while (edge !== null) {
|
480
|
+
nodes.push(this.#edges.from(edge));
|
481
|
+
edge = this.#edges.nextIn(edge);
|
482
|
+
}
|
483
|
+
}
|
484
|
+
node = this.#nodes.next(node);
|
485
|
+
}
|
486
|
+
return nodes;
|
487
|
+
}
|
488
|
+
|
489
|
+
inspect(): any {
|
490
|
+
return {
|
491
|
+
nodes: this.#nodes.inspect(),
|
492
|
+
edges: this.#edges.inspect(),
|
493
|
+
};
|
494
|
+
}
|
495
|
+
}
|
496
|
+
|
497
|
+
/**
|
498
|
+
* `SharedTypeMap` is a hashmap of items,
|
499
|
+
* where each item has its own 'type' field.
|
500
|
+
*
|
501
|
+
* The `SharedTypeMap` is backed by a shared array buffer of fixed length.
|
502
|
+
* The buffer is partitioned into:
|
503
|
+
* - a header, which stores the capacity and number of items in the map,
|
504
|
+
* - a hash table, which is an array of pointers to linked lists of items
|
505
|
+
* with the same hash,
|
506
|
+
* - an items array, which is where the linked items are stored.
|
507
|
+
*
|
508
|
+
* hash table item
|
509
|
+
* (capacity) (ITEM_SIZE)
|
510
|
+
* ┌──────┴──────┐ ┌──┴──┐
|
511
|
+
* ┌──┬──┬──┬───────┬──┬──┬──┬───────┬──┬──┐
|
512
|
+
* │ │ │ │ ... │ │ │ │ ... │ │ │
|
513
|
+
* └──┴──┴──┴───────┴──┴──┴──┴───────┴──┴──┘
|
514
|
+
* └──┬──┘ └─────────┬─────────┘
|
515
|
+
* header items
|
516
|
+
* (HEADER_SIZE) (capacity * ITEM_SIZE * BUCKET_SIZE)
|
517
|
+
*
|
518
|
+
*
|
519
|
+
* An item is added with a hash key that fits within the range of the hash
|
520
|
+
* table capacity. The item is stored at the next available address after the
|
521
|
+
* hash table, and a pointer to the address is stored in the hash table at
|
522
|
+
* the index matching the hash. If the hash is already pointing at an item,
|
523
|
+
* the pointer is stored in the `next` field of the existing item instead.
|
524
|
+
*
|
525
|
+
* hash table items
|
526
|
+
* ┌─────────┴────────┐┌───────────────────────┴────────────────────────┐
|
527
|
+
* 0 1 2 11 17 23 29 35
|
528
|
+
* ┌───┐┌───┐┌───┐┌───┐┌───┬───┐┌───┬───┐┌───┬───┐┌───┬───┐┌───┬───┐┌───┐
|
529
|
+
* │17 ││11 ││35 ││...││23 │ 1 ││29 │ 1 ││ 0 │ 2 ││ 0 │ 2 ││ 0 │ 1 ││...│
|
530
|
+
* └───┘└───┘└───┘└───┘└───┴───┘└───┴───┘└───┴───┘└───┴───┘└───┴───┘└───┘
|
531
|
+
* │ │ │ ▲ ▲ ▲ ▲ ▲
|
532
|
+
* └────┼────┼─────────┼────────┴────────┼────────┘ │
|
533
|
+
* └────┼─────────┴─────────────────┘ │
|
534
|
+
* └─────────────────────────────────────────────┘
|
535
|
+
*/
|
536
|
+
export class SharedTypeMap<TItemType, THash, TAddress: number>
|
537
|
+
implements Iterable<TAddress>
|
538
|
+
{
|
539
|
+
/**
|
540
|
+
* The header for the `SharedTypeMap` comprises 2 4-byte chunks:
|
541
|
+
*
|
542
|
+
* struct SharedTypeMapHeader {
|
543
|
+
* int capacity;
|
544
|
+
* int count;
|
545
|
+
* }
|
546
|
+
*
|
547
|
+
* ┌──────────┬───────┐
|
548
|
+
* │ CAPACITY │ COUNT │
|
549
|
+
* └──────────┴───────┘
|
550
|
+
*/
|
551
|
+
static HEADER_SIZE: number = 2;
|
552
|
+
/** The offset from the header where the capacity is stored. */
|
553
|
+
static #CAPACITY: 0 = 0;
|
554
|
+
/** The offset from the header where the count is stored. */
|
555
|
+
static #COUNT: 1 = 1;
|
556
|
+
|
557
|
+
/**
|
558
|
+
* Each item in `SharedTypeMap` comprises 2 4-byte chunks:
|
559
|
+
*
|
560
|
+
* struct Node {
|
561
|
+
* int next;
|
562
|
+
* int type;
|
563
|
+
* }
|
564
|
+
*
|
565
|
+
* ┌──────┬──────┐
|
566
|
+
* │ NEXT │ TYPE │
|
567
|
+
* └──────┴──────┘
|
568
|
+
*/
|
569
|
+
static ITEM_SIZE: number = 2;
|
570
|
+
/** The offset at which a link to the next item in the same bucket is stored. */
|
571
|
+
static #NEXT: 0 = 0;
|
572
|
+
/** The offset at which an item's type is stored. */
|
573
|
+
static #TYPE: 1 = 1;
|
574
|
+
|
575
|
+
/** The number of items to accommodate per hash bucket. */
|
576
|
+
static BUCKET_SIZE: number = 2;
|
577
|
+
|
578
|
+
data: Uint32Array;
|
579
|
+
|
580
|
+
get capacity(): number {
|
581
|
+
return this.data[SharedTypeMap.#CAPACITY];
|
582
|
+
}
|
583
|
+
|
584
|
+
get count(): number {
|
585
|
+
return this.data[SharedTypeMap.#COUNT];
|
586
|
+
}
|
587
|
+
|
588
|
+
get load(): number {
|
589
|
+
return this.getLoad();
|
590
|
+
}
|
591
|
+
|
592
|
+
get length(): number {
|
593
|
+
return this.getLength();
|
594
|
+
}
|
595
|
+
|
596
|
+
get addressableLimit(): number {
|
597
|
+
return this.constructor.HEADER_SIZE + this.capacity;
|
598
|
+
}
|
599
|
+
|
600
|
+
get bufferSize(): string {
|
601
|
+
return `${(this.data.byteLength / 1024 / 1024).toLocaleString(undefined, {
|
602
|
+
minmumFractionDigits: 2,
|
603
|
+
maximumFractionDigits: 2,
|
604
|
+
})} mb`;
|
605
|
+
}
|
606
|
+
|
607
|
+
constructor(capacityOrData: number | Uint32Array) {
|
608
|
+
if (typeof capacityOrData === 'number') {
|
609
|
+
let {BYTES_PER_ELEMENT} = Uint32Array;
|
610
|
+
let CAPACITY = SharedTypeMap.#CAPACITY;
|
611
|
+
// $FlowFixMe[incompatible-call]
|
612
|
+
this.data = new Uint32Array(
|
613
|
+
new SharedArrayBuffer(
|
614
|
+
this.getLength(capacityOrData) * BYTES_PER_ELEMENT,
|
615
|
+
),
|
616
|
+
);
|
617
|
+
this.data[CAPACITY] = capacityOrData;
|
618
|
+
} else {
|
619
|
+
this.data = capacityOrData;
|
620
|
+
assert(this.getLength() === this.data.length, 'Data appears corrupt.');
|
621
|
+
}
|
622
|
+
}
|
623
|
+
|
624
|
+
set(data: Uint32Array): void {
|
625
|
+
let {HEADER_SIZE, ITEM_SIZE} = this.constructor;
|
626
|
+
let NEXT = SharedTypeMap.#NEXT;
|
627
|
+
let COUNT = SharedTypeMap.#COUNT;
|
628
|
+
let CAPACITY = SharedTypeMap.#CAPACITY;
|
629
|
+
|
630
|
+
let delta = this.capacity - data[CAPACITY];
|
631
|
+
assert(delta >= 0, 'Cannot copy to a map with smaller capacity.');
|
632
|
+
|
633
|
+
// Copy the header.
|
634
|
+
this.data.set(data.subarray(COUNT, HEADER_SIZE), COUNT);
|
635
|
+
|
636
|
+
// Copy the hash table.
|
637
|
+
let toTable = this.data.subarray(HEADER_SIZE, HEADER_SIZE + this.capacity);
|
638
|
+
toTable.set(data.subarray(HEADER_SIZE, HEADER_SIZE + data[CAPACITY]));
|
639
|
+
// Offset first links to account for the change in table capacity.
|
640
|
+
let max = toTable.length;
|
641
|
+
for (let i = 0; i < max; i++) {
|
642
|
+
if (toTable[i]) toTable[i] += delta;
|
643
|
+
}
|
644
|
+
|
645
|
+
// Copy the items.
|
646
|
+
let toItems = this.data.subarray(HEADER_SIZE + this.capacity);
|
647
|
+
toItems.set(data.subarray(HEADER_SIZE + data[CAPACITY]));
|
648
|
+
// Offset next links to account for the change in table capacity.
|
649
|
+
max = toItems.length;
|
650
|
+
for (let i = 0; i < max; i += ITEM_SIZE) {
|
651
|
+
if (toItems[i + NEXT]) toItems[i + NEXT] += delta;
|
652
|
+
}
|
653
|
+
}
|
654
|
+
|
655
|
+
getLoad(count: number = this.count): number {
|
656
|
+
let {BUCKET_SIZE} = this.constructor;
|
657
|
+
return count / (this.capacity * BUCKET_SIZE);
|
658
|
+
}
|
659
|
+
|
660
|
+
getLength(capacity: number = this.capacity): number {
|
661
|
+
let {HEADER_SIZE, ITEM_SIZE, BUCKET_SIZE} = this.constructor;
|
662
|
+
return capacity + HEADER_SIZE + ITEM_SIZE * BUCKET_SIZE * capacity;
|
663
|
+
}
|
664
|
+
|
665
|
+
/** Get the next available address in the map. */
|
666
|
+
getNextAddress(): TAddress {
|
667
|
+
let {HEADER_SIZE, ITEM_SIZE} = this.constructor;
|
668
|
+
return (HEADER_SIZE + this.capacity + this.count * ITEM_SIZE: any);
|
669
|
+
}
|
670
|
+
|
671
|
+
/** Get the address of the first item with the given hash. */
|
672
|
+
head(hash: THash): TAddress | null {
|
673
|
+
let {HEADER_SIZE} = this.constructor;
|
674
|
+
return (this.data[HEADER_SIZE + (hash: any)]: any) || null;
|
675
|
+
}
|
676
|
+
|
677
|
+
/** Get the address of the next item with the same hash as the given item. */
|
678
|
+
next(item: TAddress): TAddress | null {
|
679
|
+
let NEXT = SharedTypeMap.#NEXT;
|
680
|
+
return (this.data[(item: any) + NEXT]: any) || null;
|
681
|
+
}
|
682
|
+
|
683
|
+
typeOf(item: TAddress): TItemType {
|
684
|
+
return (this.data[item + SharedTypeMap.#TYPE]: any);
|
685
|
+
}
|
686
|
+
|
687
|
+
link(hash: THash, item: TAddress, type: TItemType): void {
|
688
|
+
let COUNT = SharedTypeMap.#COUNT;
|
689
|
+
let NEXT = SharedTypeMap.#NEXT;
|
690
|
+
let TYPE = SharedTypeMap.#TYPE;
|
691
|
+
let {HEADER_SIZE} = this.constructor;
|
692
|
+
|
693
|
+
this.data[item + TYPE] = (type: any);
|
694
|
+
|
695
|
+
let prev = this.head(hash);
|
696
|
+
if (prev !== null) {
|
697
|
+
let next = this.next(prev);
|
698
|
+
while (next !== null) {
|
699
|
+
prev = next;
|
700
|
+
next = this.next(next);
|
701
|
+
}
|
702
|
+
this.data[prev + NEXT] = item;
|
703
|
+
} else {
|
704
|
+
// This is the first item in the bucket!
|
705
|
+
this.data[HEADER_SIZE + (hash: any)] = item;
|
706
|
+
}
|
707
|
+
this.data[COUNT]++;
|
708
|
+
}
|
709
|
+
|
710
|
+
unlink(hash: THash, item: TAddress): void {
|
711
|
+
let COUNT = SharedTypeMap.#COUNT;
|
712
|
+
let NEXT = SharedTypeMap.#NEXT;
|
713
|
+
let TYPE = SharedTypeMap.#TYPE;
|
714
|
+
let {HEADER_SIZE} = this.constructor;
|
715
|
+
|
716
|
+
this.data[item + TYPE] = 0;
|
717
|
+
|
718
|
+
let head = this.head(hash);
|
719
|
+
// No bucket to unlink from.
|
720
|
+
if (head === null) return;
|
721
|
+
|
722
|
+
let next = this.next(item);
|
723
|
+
let prev = null;
|
724
|
+
let candidate = head;
|
725
|
+
while (candidate !== null && candidate !== item) {
|
726
|
+
prev = candidate;
|
727
|
+
candidate = this.next(candidate);
|
728
|
+
}
|
729
|
+
if (prev !== null && next !== null) {
|
730
|
+
this.data[prev + NEXT] = next;
|
731
|
+
} else if (prev !== null) {
|
732
|
+
this.data[prev + NEXT] = 0;
|
733
|
+
} else if (next !== null) {
|
734
|
+
this.data[HEADER_SIZE + (hash: any)] = next;
|
735
|
+
} else {
|
736
|
+
this.data[HEADER_SIZE + (hash: any)] = 0;
|
737
|
+
}
|
738
|
+
this.data[item + NEXT] = 0;
|
739
|
+
this.data[COUNT]--;
|
740
|
+
}
|
741
|
+
|
742
|
+
forEach(cb: (item: TAddress) => void): void {
|
743
|
+
let max = this.count;
|
744
|
+
let len = this.length;
|
745
|
+
let {ITEM_SIZE} = this.constructor;
|
746
|
+
for (
|
747
|
+
let i = this.addressableLimit, count = 0;
|
748
|
+
i < len && count < max;
|
749
|
+
i += ITEM_SIZE
|
750
|
+
) {
|
751
|
+
// Skip items that don't have a type.
|
752
|
+
if (this.typeOf((i: any))) {
|
753
|
+
cb((i: any));
|
754
|
+
count++;
|
755
|
+
}
|
756
|
+
}
|
757
|
+
}
|
758
|
+
|
759
|
+
// Trick Flow into believing in `Symbol.iterator`.
|
760
|
+
// See https://github.com/facebook/flow/issues/1163#issuecomment-353523840
|
761
|
+
/*:: @@iterator(): Iterator<TAddress> { return ({}: any); } */
|
762
|
+
// $FlowFixMe[unsupported-syntax]
|
763
|
+
*[Symbol.iterator](): Iterator<TAddress> {
|
764
|
+
let max = this.count;
|
765
|
+
let len = this.length;
|
766
|
+
let {ITEM_SIZE} = this.constructor;
|
767
|
+
for (
|
768
|
+
let i = this.addressableLimit, count = 0;
|
769
|
+
i < len && count < max;
|
770
|
+
i += ITEM_SIZE
|
771
|
+
) {
|
772
|
+
if (this.data.subarray(i, i + ITEM_SIZE).some(Boolean)) {
|
773
|
+
yield (i: any);
|
774
|
+
count++;
|
775
|
+
}
|
776
|
+
}
|
777
|
+
}
|
778
|
+
|
779
|
+
inspect(): {|
|
780
|
+
header: Uint32Array,
|
781
|
+
table: Uint32Array,
|
782
|
+
data: Uint32Array,
|
783
|
+
|} {
|
784
|
+
const {HEADER_SIZE, ITEM_SIZE, BUCKET_SIZE} = this.constructor;
|
785
|
+
let min = HEADER_SIZE + this.capacity;
|
786
|
+
let max = min + this.capacity * BUCKET_SIZE * ITEM_SIZE;
|
787
|
+
return {
|
788
|
+
header: this.data.subarray(0, HEADER_SIZE),
|
789
|
+
table: this.data.subarray(HEADER_SIZE, min),
|
790
|
+
data: this.data.subarray(min, max),
|
791
|
+
};
|
792
|
+
}
|
793
|
+
}
|
794
|
+
|
795
|
+
/**
|
796
|
+
* Nodes are stored in a `SharedTypeMap`, keyed on node id plus an edge type.
|
797
|
+
* This means that for any given unique node id, there may be `e` nodes in the
|
798
|
+
* map, where `e` is the number of possible edge types in the graph.
|
799
|
+
*/
|
800
|
+
export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
801
|
+
TEdgeType,
|
802
|
+
NodeId,
|
803
|
+
NodeAddress,
|
804
|
+
> {
|
805
|
+
/**
|
806
|
+
* In addition to the header defined by `SharedTypeMap`, the header for
|
807
|
+
* the node map includes a 4-byte `nextId` chunk:
|
808
|
+
*
|
809
|
+
* struct NodeTypeMapHeader {
|
810
|
+
* int capacity; // from `SharedTypeMap`
|
811
|
+
* int count; // from `SharedTypeMap`
|
812
|
+
* int nextId;
|
813
|
+
* }
|
814
|
+
*
|
815
|
+
* ┌──────────┬───────┬─────────┐
|
816
|
+
* │ CAPACITY │ COUNT │ NEXT_ID │
|
817
|
+
* └──────────┴───────┴─────────┘
|
818
|
+
*/
|
819
|
+
static HEADER_SIZE: number = 3;
|
820
|
+
/** The offset from the header where the next available node id is stored. */
|
821
|
+
static #NEXT_ID = 2;
|
822
|
+
|
823
|
+
/**
|
824
|
+
* In addition to the item fields defined by `SharedTypeMap`,
|
825
|
+
* each node includes another 4 4-byte chunks:
|
826
|
+
*
|
827
|
+
* struct Node {
|
828
|
+
* int next; // from `SharedTypeMap`
|
829
|
+
* int type; // from `SharedTypeMap`
|
830
|
+
* int firstIn;
|
831
|
+
* int firstOut;
|
832
|
+
* int lastIn;
|
833
|
+
* int lastOut;
|
834
|
+
* }
|
835
|
+
*
|
836
|
+
* ┌──────┬──────┬──────────┬───────────┬─────────┬──────────┐
|
837
|
+
* │ NEXT │ TYPE │ FIRST_IN │ FIRST_OUT │ LAST_IN │ LAST_OUT │
|
838
|
+
* └──────┴──────┴──────────┴───────────┴─────────┴──────────┘
|
839
|
+
*/
|
840
|
+
static ITEM_SIZE: number = 6;
|
841
|
+
/** The offset at which a node's first incoming edge of this type is stored. */
|
842
|
+
static #FIRST_IN = 2;
|
843
|
+
/** The offset at which a node's first outgoing edge of this type is stored. */
|
844
|
+
static #FIRST_OUT = 3;
|
845
|
+
/** The offset at which a node's last incoming edge of this type is stored. */
|
846
|
+
static #LAST_IN = 4;
|
847
|
+
/** The offset at which a node's last outgoing edge of this type is stored. */
|
848
|
+
static #LAST_OUT = 5;
|
849
|
+
|
850
|
+
/** The smallest functional node map capacity. */
|
851
|
+
static MIN_CAPACITY: number = 2;
|
852
|
+
/** The largest possible node map capacity. */
|
853
|
+
static MAX_CAPACITY: number = Math.floor(
|
854
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
855
|
+
(2 ** 31 - 1 - NodeTypeMap.HEADER_SIZE) /
|
856
|
+
NodeTypeMap.ITEM_SIZE /
|
857
|
+
NodeTypeMap.BUCKET_SIZE,
|
858
|
+
);
|
859
|
+
|
860
|
+
get nextId(): NodeId {
|
861
|
+
return toNodeId(this.data[NodeTypeMap.#NEXT_ID]);
|
862
|
+
}
|
863
|
+
set nextId(nextId: NodeId) {
|
864
|
+
this.data[NodeTypeMap.#NEXT_ID] = fromNodeId(nextId);
|
865
|
+
}
|
866
|
+
|
867
|
+
/** Get a unique node id. */
|
868
|
+
getId(): NodeId {
|
869
|
+
return toNodeId(this.data[NodeTypeMap.#NEXT_ID]++);
|
870
|
+
}
|
871
|
+
|
872
|
+
getLoad(count: number = this.count): number {
|
873
|
+
return Math.max(
|
874
|
+
fromNodeId(this.nextId) / this.capacity,
|
875
|
+
super.getLoad(count),
|
876
|
+
);
|
877
|
+
}
|
878
|
+
|
879
|
+
add(node: NodeId, type: TEdgeType): NodeAddress {
|
880
|
+
let index = fromNodeId(node);
|
881
|
+
assert(
|
882
|
+
index >= 0 && index < this.data[NodeTypeMap.#NEXT_ID],
|
883
|
+
`Invalid node id ${String(node)} (${this.data[NodeTypeMap.#NEXT_ID]})`,
|
884
|
+
);
|
885
|
+
let address = this.getNextAddress();
|
886
|
+
this.link(node, address, type);
|
887
|
+
return address;
|
888
|
+
}
|
889
|
+
|
890
|
+
addressOf(node: NodeId, type: TEdgeType): NodeAddress | null {
|
891
|
+
let address = this.head(node);
|
892
|
+
while (address !== null) {
|
893
|
+
if (this.typeOf(address) === type) {
|
894
|
+
return address;
|
895
|
+
}
|
896
|
+
address = this.next(address);
|
897
|
+
}
|
898
|
+
return null;
|
899
|
+
}
|
900
|
+
|
901
|
+
firstIn(node: NodeAddress): EdgeAddress | null {
|
902
|
+
return this.data[node + NodeTypeMap.#FIRST_IN] || null;
|
903
|
+
}
|
904
|
+
|
905
|
+
firstOut(node: NodeAddress): EdgeAddress | null {
|
906
|
+
return this.data[node + NodeTypeMap.#FIRST_OUT] || null;
|
907
|
+
}
|
908
|
+
|
909
|
+
lastIn(node: NodeAddress): EdgeAddress | null {
|
910
|
+
return this.data[node + NodeTypeMap.#LAST_IN] || null;
|
911
|
+
}
|
912
|
+
|
913
|
+
lastOut(node: NodeAddress): EdgeAddress | null {
|
914
|
+
return this.data[node + NodeTypeMap.#LAST_OUT] || null;
|
915
|
+
}
|
916
|
+
|
917
|
+
linkIn(node: NodeAddress, edge: EdgeAddress): EdgeAddress | null {
|
918
|
+
let first = this.firstIn(node);
|
919
|
+
let last = this.lastIn(node);
|
920
|
+
if (first === null) this.data[node + NodeTypeMap.#FIRST_IN] = edge;
|
921
|
+
this.data[node + NodeTypeMap.#LAST_IN] = edge;
|
922
|
+
return last;
|
923
|
+
}
|
924
|
+
|
925
|
+
unlinkIn(
|
926
|
+
node: NodeAddress,
|
927
|
+
edge: EdgeAddress,
|
928
|
+
prev: EdgeAddress | null,
|
929
|
+
next: EdgeAddress | null,
|
930
|
+
): void {
|
931
|
+
let first = this.firstIn(node);
|
932
|
+
let last = this.lastIn(node);
|
933
|
+
if (last === edge) {
|
934
|
+
this.data[node + NodeTypeMap.#LAST_IN] = prev === null ? 0 : prev;
|
935
|
+
}
|
936
|
+
if (first === edge) {
|
937
|
+
this.data[node + NodeTypeMap.#FIRST_IN] = next === null ? 0 : next;
|
938
|
+
}
|
939
|
+
}
|
940
|
+
|
941
|
+
linkOut(node: NodeAddress, edge: EdgeAddress): EdgeAddress | null {
|
942
|
+
let first = this.firstOut(node);
|
943
|
+
let last = this.lastOut(node);
|
944
|
+
if (first === null) this.data[node + NodeTypeMap.#FIRST_OUT] = edge;
|
945
|
+
this.data[node + NodeTypeMap.#LAST_OUT] = edge;
|
946
|
+
return last;
|
947
|
+
}
|
948
|
+
|
949
|
+
unlinkOut(
|
950
|
+
node: NodeAddress,
|
951
|
+
edge: EdgeAddress,
|
952
|
+
prev: EdgeAddress | null,
|
953
|
+
next: EdgeAddress | null,
|
954
|
+
): void {
|
955
|
+
let first = this.firstOut(node);
|
956
|
+
let last = this.lastOut(node);
|
957
|
+
if (last === edge) {
|
958
|
+
this.data[node + NodeTypeMap.#LAST_OUT] = prev === null ? 0 : prev;
|
959
|
+
}
|
960
|
+
if (first === edge) {
|
961
|
+
this.data[node + NodeTypeMap.#FIRST_OUT] = next === null ? 0 : next;
|
962
|
+
}
|
963
|
+
}
|
964
|
+
}
|
965
|
+
|
966
|
+
/**
|
967
|
+
* Edges are stored in a `SharedTypeMap`,
|
968
|
+
* keyed on the 'from' and 'to' node ids, and the edge type.
|
969
|
+
*/
|
970
|
+
export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
971
|
+
TEdgeType,
|
972
|
+
EdgeHash,
|
973
|
+
EdgeAddress,
|
974
|
+
> {
|
975
|
+
/**
|
976
|
+
* In addition to the header defined by `SharedTypeMap`, the header for
|
977
|
+
* the edge map includes a 4-byte `deletes` chunk:
|
978
|
+
*
|
979
|
+
* struct EdgeTypeMapHeader {
|
980
|
+
* int capacity; // from `SharedTypeMap`
|
981
|
+
* int count; // from `SharedTypeMap`
|
982
|
+
* int deletes;
|
983
|
+
* }
|
984
|
+
*
|
985
|
+
* ┌──────────┬───────┬─────────┐
|
986
|
+
* │ CAPACITY │ COUNT │ DELETES │
|
987
|
+
* └──────────┴───────┴─────────┘
|
988
|
+
*/
|
989
|
+
static HEADER_SIZE: number = 3;
|
990
|
+
/** The offset from the header where the delete count is stored. */
|
991
|
+
static #DELETES = 2;
|
992
|
+
|
993
|
+
/**
|
994
|
+
* In addition to the item fields defined by `SharedTypeMap`,
|
995
|
+
* each edge includes another 6 4-byte chunks:
|
996
|
+
*
|
997
|
+
* struct Edge {
|
998
|
+
* int next; // from `SharedTypeMap`
|
999
|
+
* int type; // from `SharedTypeMap`
|
1000
|
+
* int from;
|
1001
|
+
* int to;
|
1002
|
+
* int nextIn;
|
1003
|
+
* int prevIn;
|
1004
|
+
* int nextOut;
|
1005
|
+
* int prevOut;
|
1006
|
+
* }
|
1007
|
+
*
|
1008
|
+
* ┌──────┬──────┬──────┬────┬─────────┬─────────┬──────────┬──────────┐
|
1009
|
+
* │ NEXT │ TYPE │ FROM │ TO │ NEXT_IN │ PREV_IN │ NEXT_OUT │ PREV_OUT │
|
1010
|
+
* └──────┴──────┴──────┴────┴─────────┴─────────┴──────────┴──────────┘
|
1011
|
+
*/
|
1012
|
+
static ITEM_SIZE: number = 8;
|
1013
|
+
/** The offset at which an edge's 'from' node id is stored. */
|
1014
|
+
static #FROM = 2;
|
1015
|
+
/** The offset at which an edge's 'to' node id is stored. */
|
1016
|
+
static #TO = 3;
|
1017
|
+
/** The offset at which the 'to' node's next incoming edge is stored. */
|
1018
|
+
static #NEXT_IN = 4;
|
1019
|
+
/** The offset at which the 'to' node's previous incoming edge is stored. */
|
1020
|
+
static #PREV_IN = 5;
|
1021
|
+
/** The offset at which the 'from' node's next outgoing edge is stored. */
|
1022
|
+
static #NEXT_OUT = 6;
|
1023
|
+
/** The offset at which the 'from' node's previous outgoing edge is stored. */
|
1024
|
+
static #PREV_OUT = 7;
|
1025
|
+
|
1026
|
+
/** The smallest functional edge map capacity. */
|
1027
|
+
static MIN_CAPACITY: number = 2;
|
1028
|
+
/** The largest possible edge map capacity. */
|
1029
|
+
static MAX_CAPACITY: number = Math.floor(
|
1030
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
1031
|
+
(2 ** 31 - 1 - EdgeTypeMap.HEADER_SIZE) /
|
1032
|
+
EdgeTypeMap.ITEM_SIZE /
|
1033
|
+
EdgeTypeMap.BUCKET_SIZE,
|
1034
|
+
);
|
1035
|
+
/** The size after which to grow the capacity by the minimum factor. */
|
1036
|
+
static PEAK_CAPACITY: number = 2 ** 18;
|
1037
|
+
|
1038
|
+
get deletes(): number {
|
1039
|
+
return this.data[EdgeTypeMap.#DELETES];
|
1040
|
+
}
|
1041
|
+
|
1042
|
+
getNextAddress(): EdgeAddress {
|
1043
|
+
let {ITEM_SIZE} = this.constructor;
|
1044
|
+
return this.addressableLimit + (this.count + this.deletes) * ITEM_SIZE;
|
1045
|
+
}
|
1046
|
+
|
1047
|
+
add(hash: EdgeHash, from: NodeId, to: NodeId, type: TEdgeType): EdgeAddress {
|
1048
|
+
assert(
|
1049
|
+
hash >= 0 && hash < this.capacity,
|
1050
|
+
`Invalid edge hash ${String(hash)}`,
|
1051
|
+
);
|
1052
|
+
// Use the next available edge address.
|
1053
|
+
let edge = this.getNextAddress();
|
1054
|
+
// Add our new edge to its hash bucket.
|
1055
|
+
this.link(hash, edge, type);
|
1056
|
+
this.data[edge + EdgeTypeMap.#FROM] = fromNodeId(from);
|
1057
|
+
this.data[edge + EdgeTypeMap.#TO] = fromNodeId(to);
|
1058
|
+
return edge;
|
1059
|
+
}
|
1060
|
+
|
1061
|
+
delete(edge: EdgeAddress): void {
|
1062
|
+
this.data[edge + EdgeTypeMap.#FROM] = 0;
|
1063
|
+
this.data[edge + EdgeTypeMap.#TO] = 0;
|
1064
|
+
this.data[EdgeTypeMap.#DELETES]++;
|
1065
|
+
}
|
1066
|
+
|
1067
|
+
addressOf(
|
1068
|
+
hash: EdgeHash,
|
1069
|
+
from: NodeId,
|
1070
|
+
to: NodeId,
|
1071
|
+
type: TEdgeType,
|
1072
|
+
): EdgeAddress | null {
|
1073
|
+
let address = this.head(hash);
|
1074
|
+
while (address !== null) {
|
1075
|
+
if (
|
1076
|
+
this.typeOf(address) === type &&
|
1077
|
+
this.from(address) === from &&
|
1078
|
+
this.to(address) === to
|
1079
|
+
) {
|
1080
|
+
return address;
|
1081
|
+
}
|
1082
|
+
address = this.next(address);
|
1083
|
+
}
|
1084
|
+
return null;
|
1085
|
+
}
|
1086
|
+
|
1087
|
+
from(edge: EdgeAddress): NodeId {
|
1088
|
+
return toNodeId(this.data[edge + EdgeTypeMap.#FROM]);
|
1089
|
+
}
|
1090
|
+
|
1091
|
+
to(edge: EdgeAddress): NodeId {
|
1092
|
+
return toNodeId(this.data[edge + EdgeTypeMap.#TO]);
|
1093
|
+
}
|
1094
|
+
|
1095
|
+
nextIn(edge: EdgeAddress): EdgeAddress | null {
|
1096
|
+
return this.data[edge + EdgeTypeMap.#NEXT_IN] || null;
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
prevIn(edge: EdgeAddress): EdgeAddress | null {
|
1100
|
+
return this.data[edge + EdgeTypeMap.#PREV_IN] || null;
|
1101
|
+
}
|
1102
|
+
|
1103
|
+
linkIn(edge: EdgeAddress, next: EdgeAddress) {
|
1104
|
+
this.data[edge + EdgeTypeMap.#NEXT_IN] = next;
|
1105
|
+
this.data[next + EdgeTypeMap.#PREV_IN] = edge;
|
1106
|
+
}
|
1107
|
+
|
1108
|
+
unlinkIn(edge: EdgeAddress) {
|
1109
|
+
let next = this.nextIn(edge);
|
1110
|
+
let prev = this.prevIn(edge);
|
1111
|
+
this.data[edge + EdgeTypeMap.#NEXT_IN] = 0;
|
1112
|
+
this.data[edge + EdgeTypeMap.#PREV_IN] = 0;
|
1113
|
+
if (next !== null && prev !== null) {
|
1114
|
+
this.data[prev + EdgeTypeMap.#NEXT_IN] = next;
|
1115
|
+
this.data[next + EdgeTypeMap.#PREV_IN] = prev;
|
1116
|
+
} else if (next !== null) {
|
1117
|
+
this.data[next + EdgeTypeMap.#PREV_IN] = 0;
|
1118
|
+
} else if (prev !== null) {
|
1119
|
+
this.data[prev + EdgeTypeMap.#NEXT_IN] = 0;
|
1120
|
+
}
|
1121
|
+
}
|
1122
|
+
|
1123
|
+
nextOut(edge: EdgeAddress): EdgeAddress | null {
|
1124
|
+
return this.data[edge + EdgeTypeMap.#NEXT_OUT] || null;
|
1125
|
+
}
|
1126
|
+
|
1127
|
+
prevOut(edge: EdgeAddress): EdgeAddress | null {
|
1128
|
+
return this.data[edge + EdgeTypeMap.#PREV_OUT] || null;
|
1129
|
+
}
|
1130
|
+
|
1131
|
+
linkOut(edge: EdgeAddress, next: EdgeAddress) {
|
1132
|
+
this.data[edge + EdgeTypeMap.#NEXT_OUT] = next;
|
1133
|
+
this.data[next + EdgeTypeMap.#PREV_OUT] = edge;
|
1134
|
+
}
|
1135
|
+
|
1136
|
+
unlinkOut(edge: EdgeAddress) {
|
1137
|
+
let next = this.nextOut(edge);
|
1138
|
+
let prev = this.prevOut(edge);
|
1139
|
+
this.data[edge + EdgeTypeMap.#NEXT_OUT] = 0;
|
1140
|
+
this.data[edge + EdgeTypeMap.#PREV_OUT] = 0;
|
1141
|
+
if (next !== null && prev !== null) {
|
1142
|
+
this.data[prev + EdgeTypeMap.#NEXT_OUT] = next;
|
1143
|
+
this.data[next + EdgeTypeMap.#PREV_OUT] = prev;
|
1144
|
+
} else if (next !== null) {
|
1145
|
+
this.data[next + EdgeTypeMap.#PREV_OUT] = 0;
|
1146
|
+
} else if (prev !== null) {
|
1147
|
+
this.data[prev + EdgeTypeMap.#NEXT_OUT] = 0;
|
1148
|
+
}
|
1149
|
+
}
|
1150
|
+
|
1151
|
+
/** Create a hash of the edge connecting the `from` and `to` nodes. */
|
1152
|
+
hash(from: NodeId, to: NodeId, type: TEdgeType): EdgeHash {
|
1153
|
+
// Each parameter is hashed by mixing its upper bits into its lower bits to
|
1154
|
+
// increase the likelihood that a change to any bit of the input will vary
|
1155
|
+
// the output widely. Then we do a series of prime multiplications and
|
1156
|
+
// additions to combine the hashes into one value.
|
1157
|
+
let hash = 17;
|
1158
|
+
hash = hash * 37 + hash32shift((from: any));
|
1159
|
+
hash = hash * 37 + hash32shift((to: any));
|
1160
|
+
hash = hash * 37 + hash32shift((type: any));
|
1161
|
+
// Finally, we map the hash to a value modulo the edge capacity.
|
1162
|
+
hash %= this.capacity;
|
1163
|
+
return hash;
|
1164
|
+
}
|
1165
|
+
}
|
1166
|
+
|
1167
|
+
// From https://gist.github.com/badboy/6267743#32-bit-mix-functions
|
1168
|
+
function hash32shift(key: number): number {
|
1169
|
+
key = ~key + (key << 15); // key = (key << 15) - key - 1;
|
1170
|
+
key = key ^ (key >> 12);
|
1171
|
+
key = key + (key << 2);
|
1172
|
+
key = key ^ (key >> 4);
|
1173
|
+
key = key * 2057; // key = (key + (key << 3)) + (key << 11);
|
1174
|
+
key = key ^ (key >> 16);
|
1175
|
+
return key;
|
1176
|
+
}
|
1177
|
+
|
1178
|
+
function interpolate(x: number, y: number, t: number): number {
|
1179
|
+
return x + (y - x) * Math.min(1, Math.max(0, t));
|
1180
|
+
}
|
1181
|
+
|
1182
|
+
function increaseNodeCapacity(nodeCapacity: number): number {
|
1183
|
+
let {MIN_CAPACITY, MAX_CAPACITY} = NodeTypeMap;
|
1184
|
+
let newCapacity = Math.round(nodeCapacity * MIN_GROW_FACTOR);
|
1185
|
+
assert(newCapacity <= MAX_CAPACITY, 'Node capacity overflow!');
|
1186
|
+
return Math.max(MIN_CAPACITY, newCapacity);
|
1187
|
+
}
|
1188
|
+
|
1189
|
+
function getNextEdgeCapacity(
|
1190
|
+
capacity: number,
|
1191
|
+
count: number,
|
1192
|
+
load: number,
|
1193
|
+
): number {
|
1194
|
+
let {MIN_CAPACITY, MAX_CAPACITY, PEAK_CAPACITY} = EdgeTypeMap;
|
1195
|
+
let newCapacity = capacity;
|
1196
|
+
if (load > LOAD_FACTOR) {
|
1197
|
+
// This is intended to strike a balance between growing the edge capacity
|
1198
|
+
// in too small increments, which causes a lot of resizing, and growing
|
1199
|
+
// the edge capacity in too large increments, which results in a lot of
|
1200
|
+
// wasted memory.
|
1201
|
+
let pct = capacity / PEAK_CAPACITY;
|
1202
|
+
let growFactor = interpolate(MAX_GROW_FACTOR, MIN_GROW_FACTOR, pct);
|
1203
|
+
newCapacity = Math.round(capacity * growFactor);
|
1204
|
+
} else if (load < UNLOAD_FACTOR) {
|
1205
|
+
// In some cases, it may be possible to shrink the edge capacity,
|
1206
|
+
// but this is only likely to occur when a lot of edges have been removed.
|
1207
|
+
newCapacity = Math.round(capacity * SHRINK_FACTOR);
|
1208
|
+
}
|
1209
|
+
assert(newCapacity <= MAX_CAPACITY, 'Edge capacity overflow!');
|
1210
|
+
return Math.max(MIN_CAPACITY, newCapacity);
|
1211
|
+
}
|