@ghcrawl/api-core 0.7.1 → 0.8.0-beta.1
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/dist/cluster/build.d.ts +30 -0
- package/dist/cluster/build.d.ts.map +1 -1
- package/dist/cluster/build.js +178 -7
- package/dist/cluster/build.js.map +1 -1
- package/dist/cluster/perf.integration.js +186 -20
- package/dist/cluster/perf.integration.js.map +1 -1
- package/dist/config.d.ts +9 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -2
- package/dist/config.js.map +1 -1
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +37 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/openai/provider.d.ts +2 -0
- package/dist/openai/provider.d.ts.map +1 -1
- package/dist/openai/provider.js +15 -1
- package/dist/openai/provider.js.map +1 -1
- package/dist/service.d.ts +99 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +1035 -109
- package/dist/service.js.map +1 -1
- package/dist/vector/store.d.ts +38 -0
- package/dist/vector/store.d.ts.map +1 -0
- package/dist/vector/store.js +2 -0
- package/dist/vector/store.js.map +1 -0
- package/dist/vector/vectorlite-store.d.ts +34 -0
- package/dist/vector/vectorlite-store.d.ts.map +1 -0
- package/dist/vector/vectorlite-store.js +124 -0
- package/dist/vector/vectorlite-store.js.map +1 -0
- package/package.json +7 -6
package/dist/cluster/build.d.ts
CHANGED
|
@@ -12,5 +12,35 @@ export declare function buildClusters(nodes: Node[], edges: SimilarityEdge[]): A
|
|
|
12
12
|
representativeThreadId: number;
|
|
13
13
|
members: number[];
|
|
14
14
|
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Build clusters with size-bounded Union-Find.
|
|
17
|
+
*
|
|
18
|
+
* Process edges from highest to lowest score, merging components only when
|
|
19
|
+
* the combined size stays within `maxClusterSize`. Strongest connections are
|
|
20
|
+
* preserved; weaker edges that would create oversized clusters are skipped.
|
|
21
|
+
* This avoids the "threshold raising" problem where splitting mega-clusters
|
|
22
|
+
* creates many solos.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildSizeBoundedClusters(nodes: Node[], edges: SimilarityEdge[], options: {
|
|
25
|
+
maxClusterSize: number;
|
|
26
|
+
}): Array<{
|
|
27
|
+
representativeThreadId: number;
|
|
28
|
+
members: number[];
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Build clusters with iterative refinement of oversized components.
|
|
32
|
+
*
|
|
33
|
+
* 1. Run Union-Find at the base threshold (edges already filtered by minScore).
|
|
34
|
+
* 2. For any cluster above `maxClusterSize`, re-cluster its members using only
|
|
35
|
+
* edges above a progressively higher threshold (raised by `refineStep` each
|
|
36
|
+
* iteration) until all clusters are within limits or threshold reaches 1.0.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildRefinedClusters(nodes: Node[], edges: SimilarityEdge[], options: {
|
|
39
|
+
maxClusterSize: number;
|
|
40
|
+
refineStep: number;
|
|
41
|
+
}): Array<{
|
|
42
|
+
representativeThreadId: number;
|
|
43
|
+
members: number[];
|
|
44
|
+
}>;
|
|
15
45
|
export {};
|
|
16
46
|
//# sourceMappingURL=build.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cluster/build.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,IAAI,GAAG;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cluster/build.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,IAAI,GAAG;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AA4DF,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAclI;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,IAAI,EAAE,EACb,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAClC,KAAK,CAAC;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuB9D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,IAAI,EAAE,EACb,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACtD,KAAK,CAAC;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA0F9D"}
|
package/dist/cluster/build.js
CHANGED
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
class UnionFind {
|
|
2
2
|
parent = new Map();
|
|
3
|
+
size = new Map();
|
|
3
4
|
add(value) {
|
|
4
|
-
if (!this.parent.has(value))
|
|
5
|
+
if (!this.parent.has(value)) {
|
|
5
6
|
this.parent.set(value, value);
|
|
7
|
+
this.size.set(value, 1);
|
|
8
|
+
}
|
|
6
9
|
}
|
|
7
10
|
find(value) {
|
|
8
|
-
|
|
11
|
+
let parent = this.parent.get(value);
|
|
9
12
|
if (parent === undefined) {
|
|
10
13
|
this.parent.set(value, value);
|
|
14
|
+
this.size.set(value, 1);
|
|
11
15
|
return value;
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
// Iterative path-finding to avoid stack overflow on deep chains
|
|
18
|
+
let current = value;
|
|
19
|
+
while (parent !== current) {
|
|
20
|
+
const grandparent = this.parent.get(parent) ?? parent;
|
|
21
|
+
this.parent.set(current, grandparent); // path splitting
|
|
22
|
+
current = parent;
|
|
23
|
+
parent = grandparent;
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
18
26
|
}
|
|
19
27
|
union(left, right) {
|
|
20
28
|
const leftRoot = this.find(left);
|
|
21
29
|
const rightRoot = this.find(right);
|
|
22
30
|
if (leftRoot !== rightRoot) {
|
|
31
|
+
const leftSize = this.size.get(leftRoot) ?? 1;
|
|
32
|
+
const rightSize = this.size.get(rightRoot) ?? 1;
|
|
23
33
|
this.parent.set(rightRoot, leftRoot);
|
|
34
|
+
this.size.set(leftRoot, leftSize + rightSize);
|
|
24
35
|
}
|
|
25
36
|
}
|
|
37
|
+
/** Merge only if the combined component would not exceed maxSize. Returns true if merged. */
|
|
38
|
+
unionBounded(left, right, maxSize) {
|
|
39
|
+
const leftRoot = this.find(left);
|
|
40
|
+
const rightRoot = this.find(right);
|
|
41
|
+
if (leftRoot === rightRoot)
|
|
42
|
+
return true; // already same component
|
|
43
|
+
const leftSize = this.size.get(leftRoot) ?? 1;
|
|
44
|
+
const rightSize = this.size.get(rightRoot) ?? 1;
|
|
45
|
+
if (leftSize + rightSize > maxSize)
|
|
46
|
+
return false;
|
|
47
|
+
this.parent.set(rightRoot, leftRoot);
|
|
48
|
+
this.size.set(leftRoot, leftSize + rightSize);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
getSize(value) {
|
|
52
|
+
return this.size.get(this.find(value)) ?? 1;
|
|
53
|
+
}
|
|
26
54
|
}
|
|
27
55
|
export function buildClusters(nodes, edges) {
|
|
28
56
|
const uf = new UnionFind();
|
|
@@ -37,6 +65,149 @@ export function buildClusters(nodes, edges) {
|
|
|
37
65
|
list.push(node.threadId);
|
|
38
66
|
byRoot.set(root, list);
|
|
39
67
|
}
|
|
68
|
+
return formatClusters(nodes, edges, byRoot);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build clusters with size-bounded Union-Find.
|
|
72
|
+
*
|
|
73
|
+
* Process edges from highest to lowest score, merging components only when
|
|
74
|
+
* the combined size stays within `maxClusterSize`. Strongest connections are
|
|
75
|
+
* preserved; weaker edges that would create oversized clusters are skipped.
|
|
76
|
+
* This avoids the "threshold raising" problem where splitting mega-clusters
|
|
77
|
+
* creates many solos.
|
|
78
|
+
*/
|
|
79
|
+
export function buildSizeBoundedClusters(nodes, edges, options) {
|
|
80
|
+
const uf = new UnionFind();
|
|
81
|
+
for (const node of nodes)
|
|
82
|
+
uf.add(node.threadId);
|
|
83
|
+
// Sort edges by score descending — strongest connections first
|
|
84
|
+
const sortedEdges = [...edges].sort((a, b) => b.score - a.score);
|
|
85
|
+
const keptEdges = [];
|
|
86
|
+
for (const edge of sortedEdges) {
|
|
87
|
+
if (uf.unionBounded(edge.leftThreadId, edge.rightThreadId, options.maxClusterSize)) {
|
|
88
|
+
keptEdges.push(edge);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const byRoot = new Map();
|
|
92
|
+
for (const node of nodes) {
|
|
93
|
+
const root = uf.find(node.threadId);
|
|
94
|
+
const list = byRoot.get(root) ?? [];
|
|
95
|
+
list.push(node.threadId);
|
|
96
|
+
byRoot.set(root, list);
|
|
97
|
+
}
|
|
98
|
+
return formatClusters(nodes, keptEdges, byRoot);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build clusters with iterative refinement of oversized components.
|
|
102
|
+
*
|
|
103
|
+
* 1. Run Union-Find at the base threshold (edges already filtered by minScore).
|
|
104
|
+
* 2. For any cluster above `maxClusterSize`, re-cluster its members using only
|
|
105
|
+
* edges above a progressively higher threshold (raised by `refineStep` each
|
|
106
|
+
* iteration) until all clusters are within limits or threshold reaches 1.0.
|
|
107
|
+
*/
|
|
108
|
+
export function buildRefinedClusters(nodes, edges, options) {
|
|
109
|
+
const nodesById = new Map(nodes.map((node) => [node.threadId, node]));
|
|
110
|
+
const result = [];
|
|
111
|
+
// Initial Union-Find pass
|
|
112
|
+
const uf = new UnionFind();
|
|
113
|
+
for (const node of nodes)
|
|
114
|
+
uf.add(node.threadId);
|
|
115
|
+
for (const edge of edges)
|
|
116
|
+
uf.union(edge.leftThreadId, edge.rightThreadId);
|
|
117
|
+
const byRoot = new Map();
|
|
118
|
+
for (const node of nodes) {
|
|
119
|
+
const root = uf.find(node.threadId);
|
|
120
|
+
const list = byRoot.get(root) ?? [];
|
|
121
|
+
list.push(node.threadId);
|
|
122
|
+
byRoot.set(root, list);
|
|
123
|
+
}
|
|
124
|
+
// Build adjacency list for O(E) iteration instead of O(n²) pair scanning
|
|
125
|
+
const adjacency = new Map();
|
|
126
|
+
for (const edge of edges) {
|
|
127
|
+
let list = adjacency.get(edge.leftThreadId);
|
|
128
|
+
if (!list) {
|
|
129
|
+
list = [];
|
|
130
|
+
adjacency.set(edge.leftThreadId, list);
|
|
131
|
+
}
|
|
132
|
+
list.push(edge);
|
|
133
|
+
let rList = adjacency.get(edge.rightThreadId);
|
|
134
|
+
if (!rList) {
|
|
135
|
+
rList = [];
|
|
136
|
+
adjacency.set(edge.rightThreadId, rList);
|
|
137
|
+
}
|
|
138
|
+
rList.push(edge);
|
|
139
|
+
}
|
|
140
|
+
const workQueue = [];
|
|
141
|
+
for (const members of byRoot.values()) {
|
|
142
|
+
if (members.length <= options.maxClusterSize) {
|
|
143
|
+
const clusterNodes = members.map((id) => nodesById.get(id)).filter((n) => n !== undefined);
|
|
144
|
+
const clusterEdges = edgesWithinSet(new Set(members), adjacency);
|
|
145
|
+
result.push(...formatClusters(clusterNodes, clusterEdges, new Map([[0, members]])));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
workQueue.push({ memberIds: members, currentThreshold: 0 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Iteratively refine oversized clusters
|
|
152
|
+
while (workQueue.length > 0) {
|
|
153
|
+
const item = workQueue.pop();
|
|
154
|
+
const newThreshold = item.currentThreshold + options.refineStep;
|
|
155
|
+
if (newThreshold >= 1.0) {
|
|
156
|
+
for (const memberId of item.memberIds) {
|
|
157
|
+
result.push({ representativeThreadId: memberId, members: [memberId] });
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Filter edges within this component to the higher threshold
|
|
162
|
+
const memberSet = new Set(item.memberIds);
|
|
163
|
+
const filteredEdges = [];
|
|
164
|
+
for (const memberId of item.memberIds) {
|
|
165
|
+
for (const edge of adjacency.get(memberId) ?? []) {
|
|
166
|
+
const otherId = edge.leftThreadId === memberId ? edge.rightThreadId : edge.leftThreadId;
|
|
167
|
+
if (otherId > memberId && memberSet.has(otherId) && edge.score >= newThreshold) {
|
|
168
|
+
filteredEdges.push(edge);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Re-cluster with filtered edges
|
|
173
|
+
const subUf = new UnionFind();
|
|
174
|
+
for (const memberId of item.memberIds)
|
|
175
|
+
subUf.add(memberId);
|
|
176
|
+
for (const edge of filteredEdges)
|
|
177
|
+
subUf.union(edge.leftThreadId, edge.rightThreadId);
|
|
178
|
+
const subByRoot = new Map();
|
|
179
|
+
for (const memberId of item.memberIds) {
|
|
180
|
+
const root = subUf.find(memberId);
|
|
181
|
+
const list = subByRoot.get(root) ?? [];
|
|
182
|
+
list.push(memberId);
|
|
183
|
+
subByRoot.set(root, list);
|
|
184
|
+
}
|
|
185
|
+
for (const subMembers of subByRoot.values()) {
|
|
186
|
+
if (subMembers.length <= options.maxClusterSize) {
|
|
187
|
+
const clusterNodes = subMembers.map((id) => nodesById.get(id)).filter((n) => n !== undefined);
|
|
188
|
+
const clusterEdges = edgesWithinSet(new Set(subMembers), adjacency);
|
|
189
|
+
result.push(...formatClusters(clusterNodes, clusterEdges, new Map([[0, subMembers]])));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
workQueue.push({ memberIds: subMembers, currentThreshold: newThreshold });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result.sort((left, right) => right.members.length - left.members.length);
|
|
197
|
+
}
|
|
198
|
+
function edgesWithinSet(memberSet, adjacency) {
|
|
199
|
+
const edges = [];
|
|
200
|
+
for (const memberId of memberSet) {
|
|
201
|
+
for (const edge of adjacency.get(memberId) ?? []) {
|
|
202
|
+
const otherId = edge.leftThreadId === memberId ? edge.rightThreadId : edge.leftThreadId;
|
|
203
|
+
if (otherId > memberId && memberSet.has(otherId)) {
|
|
204
|
+
edges.push(edge);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return edges;
|
|
209
|
+
}
|
|
210
|
+
function formatClusters(nodes, edges, byRoot) {
|
|
40
211
|
const edgeCounts = new Map();
|
|
41
212
|
for (const edge of edges) {
|
|
42
213
|
edgeCounts.set(edge.leftThreadId, (edgeCounts.get(edge.leftThreadId) ?? 0) + 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cluster/build.ts"],"names":[],"mappings":"AAYA,MAAM,SAAS;IACI,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cluster/build.ts"],"names":[],"mappings":"AAYA,MAAM,SAAS;IACI,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnC,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAElD,GAAG,CAAC,KAAa;QACf,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAa;QAChB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACxB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,gEAAgE;QAChE,IAAI,OAAO,GAAW,KAAK,CAAC;QAC5B,OAAO,MAAM,KAAK,OAAO,EAAE,CAAC;YAC1B,MAAM,WAAW,GAAW,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC;YAC9D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,iBAAiB;YACxD,OAAO,GAAG,MAAM,CAAC;YACjB,MAAM,GAAG,WAAW,CAAC;QACvB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,KAAa;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAChD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,6FAA6F;IAC7F,YAAY,CAAC,IAAY,EAAE,KAAa,EAAE,OAAe;QACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,CAAC,yBAAyB;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAChD,IAAI,QAAQ,GAAG,SAAS,GAAG,OAAO;YAAE,OAAO,KAAK,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,CAAC,KAAa;QACnB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,KAAuB;IAClE,MAAM,EAAE,GAAG,IAAI,SAAS,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IAE1E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CACtC,KAAa,EACb,KAAuB,EACvB,OAAmC;IAEnC,MAAM,EAAE,GAAG,IAAI,SAAS,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAEhD,+DAA+D;IAC/D,MAAM,WAAW,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACjE,MAAM,SAAS,GAAqB,EAAE,CAAC;IAEvC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;YACnF,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAa,EACb,KAAuB,EACvB,OAAuD;IAEvD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACtE,MAAM,MAAM,GAAiE,EAAE,CAAC;IAEhF,0BAA0B;IAC1B,MAAM,EAAE,GAAG,IAAI,SAAS,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IAE1E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,yEAAyE;IACzE,MAAM,SAAS,GAAG,IAAI,GAAG,EAA4B,CAAC;IACtD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;YAAC,IAAI,GAAG,EAAE,CAAC;YAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;QACjE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;YAAC,KAAK,GAAG,EAAE,CAAC;YAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;QAAC,CAAC;QACrE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAID,MAAM,SAAS,GAAe,EAAE,CAAC;IAEjC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QACtC,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;YACtG,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC;YACjE,MAAM,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtF,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,wCAAwC;IACxC,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,EAAG,CAAC;QAC9B,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;QAChE,IAAI,YAAY,IAAI,GAAG,EAAE,CAAC;YACxB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACtC,MAAM,CAAC,IAAI,CAAC,EAAE,sBAAsB,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;YACD,SAAS;QACX,CAAC;QAED,6DAA6D;QAC7D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1C,MAAM,aAAa,GAAqB,EAAE,CAAC;QAC3C,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;gBACjD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;gBACxF,IAAI,OAAO,GAAG,QAAQ,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,YAAY,EAAE,CAAC;oBAC/E,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC3B,CAAC;YACH,CAAC;QACH,CAAC;QAED,iCAAiC;QACjC,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;QAC9B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3D,KAAK,MAAM,IAAI,IAAI,aAAa;YAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAErF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC9C,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClC,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpB,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QAED,KAAK,MAAM,UAAU,IAAI,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,IAAI,UAAU,CAAC,MAAM,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;gBAChD,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;gBACzG,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,SAAS,CAAC,CAAC;gBACpE,MAAM,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACzF,CAAC;iBAAM,CAAC;gBACN,SAAS,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC,CAAC;YAC5E,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,cAAc,CAAC,SAAsB,EAAE,SAAwC;IACtF,MAAM,KAAK,GAAqB,EAAE,CAAC;IACnC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACjD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;YACxF,IAAI,OAAO,GAAG,QAAQ,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBACjD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CACrB,KAAa,EACb,KAAuB,EACvB,MAA6B;IAE7B,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChF,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACtE,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;SAC/B,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACf,MAAM,cAAc,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;YAC3D,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACrC,MAAM,SAAS,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YACjF,IAAI,SAAS,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAC;YACtC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;gBAAE,OAAO,MAAM,GAAG,OAAO,CAAC;YAC7C,OAAO,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QACpC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACN,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;IAC1G,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC"}
|
|
@@ -5,13 +5,32 @@ import path from 'node:path';
|
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { GHCrawlService } from '../service.js';
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_BASELINE_PATH = fileURLToPath(new URL('./perf-baseline.json', import.meta.url));
|
|
9
|
+
function getBaselinePath() {
|
|
10
|
+
const configuredPath = process.env.GHCRAWL_CLUSTER_PERF_CONFIG_PATH?.trim();
|
|
11
|
+
return configuredPath ? path.resolve(configuredPath) : DEFAULT_BASELINE_PATH;
|
|
12
|
+
}
|
|
9
13
|
function loadBaseline() {
|
|
10
|
-
return JSON.parse(fs.readFileSync(
|
|
14
|
+
return JSON.parse(fs.readFileSync(getBaselinePath(), 'utf8'));
|
|
11
15
|
}
|
|
12
16
|
function shouldBootstrapBaseline() {
|
|
13
17
|
return process.env.GHCRAWL_CLUSTER_PERF_BOOTSTRAP === '1';
|
|
14
18
|
}
|
|
19
|
+
function shouldIgnoreRegressionThreshold() {
|
|
20
|
+
return process.env.GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD === '1';
|
|
21
|
+
}
|
|
22
|
+
function getPerfBackend() {
|
|
23
|
+
return process.env.GHCRAWL_CLUSTER_PERF_BACKEND === 'vectorlite' ? 'vectorlite' : 'exact';
|
|
24
|
+
}
|
|
25
|
+
function assertBenchmarkShape(result, baseline, backend) {
|
|
26
|
+
if (backend === 'exact' && baseline.fixture.assertExactClusterCount !== false) {
|
|
27
|
+
assert.equal(result.clusters, baseline.fixture.clusterCount);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
assert.ok(result.clusters > 0);
|
|
31
|
+
}
|
|
32
|
+
assert.ok(result.edges > baseline.fixture.clusterCount);
|
|
33
|
+
}
|
|
15
34
|
function formatDurationMs(durationMs) {
|
|
16
35
|
if (!Number.isFinite(durationMs))
|
|
17
36
|
return 'n/a';
|
|
@@ -26,6 +45,14 @@ function formatDurationMs(durationMs) {
|
|
|
26
45
|
const seconds = totalSeconds - minutes * 60;
|
|
27
46
|
return `${minutes}m ${seconds.toFixed(1)}s`;
|
|
28
47
|
}
|
|
48
|
+
function formatBytes(bytes) {
|
|
49
|
+
if (!Number.isFinite(bytes))
|
|
50
|
+
return 'n/a';
|
|
51
|
+
if (bytes < 1024 * 1024) {
|
|
52
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
53
|
+
}
|
|
54
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
55
|
+
}
|
|
29
56
|
function formatPercent(value) {
|
|
30
57
|
const sign = value > 0 ? '+' : '';
|
|
31
58
|
return `${sign}${value.toFixed(1)}%`;
|
|
@@ -38,6 +65,22 @@ function median(values) {
|
|
|
38
65
|
}
|
|
39
66
|
return sorted[middle] ?? 0;
|
|
40
67
|
}
|
|
68
|
+
function roundFixtureMedianMs(value) {
|
|
69
|
+
return Number(value.toFixed(1));
|
|
70
|
+
}
|
|
71
|
+
function roundProjectedOpenclawMs(value) {
|
|
72
|
+
return Math.round(value);
|
|
73
|
+
}
|
|
74
|
+
function buildSuggestedBaseline(result) {
|
|
75
|
+
const shouldSuggest = result.deltaPercent < 0 || result.baselineMedianMs === result.medianMs;
|
|
76
|
+
if (!shouldSuggest) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
fixtureMedianMs: roundFixtureMedianMs(result.medianMs),
|
|
81
|
+
projectedOpenclawMs: roundProjectedOpenclawMs(result.projectedOpenclawMs),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
41
84
|
function createGitHubStub() {
|
|
42
85
|
return {
|
|
43
86
|
checkAuth: async () => undefined,
|
|
@@ -67,6 +110,8 @@ function createService(dbPath) {
|
|
|
67
110
|
openaiApiKeySource: 'none',
|
|
68
111
|
summaryModel: 'gpt-5-mini',
|
|
69
112
|
embedModel: 'text-embedding-3-large',
|
|
113
|
+
embeddingBasis: 'title_original',
|
|
114
|
+
vectorBackend: 'vectorlite',
|
|
70
115
|
embedBatchSize: 2,
|
|
71
116
|
embedConcurrency: 2,
|
|
72
117
|
embedMaxUnread: 4,
|
|
@@ -144,26 +189,64 @@ function seedBenchmarkDatabase(dbPath, baseline) {
|
|
|
144
189
|
service.close();
|
|
145
190
|
}
|
|
146
191
|
}
|
|
147
|
-
async function runSingleCluster(dbPath, baseline) {
|
|
192
|
+
async function runSingleCluster(dbPath, baseline, backend) {
|
|
148
193
|
const service = createService(dbPath);
|
|
149
194
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
// clusterExperiment may not exist on older branches (e.g. base worktree in CI)
|
|
196
|
+
if (typeof service.clusterExperiment !== 'function') {
|
|
197
|
+
const startedAt = performance.now();
|
|
198
|
+
const result = await service.clusterRepository({
|
|
199
|
+
owner: 'openclaw',
|
|
200
|
+
repo: 'openclaw',
|
|
201
|
+
k: baseline.fixture.k,
|
|
202
|
+
minScore: baseline.fixture.minScore,
|
|
203
|
+
});
|
|
204
|
+
const durationMs = performance.now() - startedAt;
|
|
205
|
+
return {
|
|
206
|
+
durationMs,
|
|
207
|
+
totalDurationMs: durationMs,
|
|
208
|
+
loadMs: 0,
|
|
209
|
+
setupMs: 0,
|
|
210
|
+
edgeBuildMs: durationMs,
|
|
211
|
+
indexBuildMs: 0,
|
|
212
|
+
queryMs: 0,
|
|
213
|
+
clusterBuildMs: 0,
|
|
214
|
+
peakRssBytes: 0,
|
|
215
|
+
peakHeapUsedBytes: 0,
|
|
216
|
+
clusters: result.clusters,
|
|
217
|
+
edges: result.edges,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const result = service.clusterExperiment({
|
|
152
221
|
owner: 'openclaw',
|
|
153
222
|
repo: 'openclaw',
|
|
223
|
+
backend,
|
|
154
224
|
k: baseline.fixture.k,
|
|
155
225
|
minScore: baseline.fixture.minScore,
|
|
156
226
|
});
|
|
157
|
-
|
|
158
|
-
|
|
227
|
+
return {
|
|
228
|
+
durationMs: result.durationMs,
|
|
229
|
+
totalDurationMs: result.totalDurationMs,
|
|
230
|
+
loadMs: result.loadMs,
|
|
231
|
+
setupMs: result.setupMs,
|
|
232
|
+
edgeBuildMs: result.edgeBuildMs,
|
|
233
|
+
indexBuildMs: result.indexBuildMs,
|
|
234
|
+
queryMs: result.queryMs,
|
|
235
|
+
clusterBuildMs: result.clusterBuildMs,
|
|
236
|
+
peakRssBytes: result.memory.peakRssBytes,
|
|
237
|
+
peakHeapUsedBytes: result.memory.peakHeapUsedBytes,
|
|
238
|
+
clusters: result.clusters,
|
|
239
|
+
edges: result.edges,
|
|
240
|
+
};
|
|
159
241
|
}
|
|
160
242
|
finally {
|
|
161
243
|
service.close();
|
|
162
244
|
}
|
|
163
245
|
}
|
|
164
246
|
async function measureBenchmark(baseline) {
|
|
247
|
+
const backend = getPerfBackend();
|
|
165
248
|
if (baseline.baseline.fixtureMedianMs <= 0 && !shouldBootstrapBaseline()) {
|
|
166
|
-
throw new Error(`Cluster perf baseline is not set in ${
|
|
249
|
+
throw new Error(`Cluster perf baseline is not set in ${getBaselinePath()}. Run the benchmark once, then record fixtureMedianMs before enforcing regressions.`);
|
|
167
250
|
}
|
|
168
251
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-cluster-perf-'));
|
|
169
252
|
const seedDbPath = path.join(tempRoot, 'seed.sqlite');
|
|
@@ -172,32 +255,76 @@ async function measureBenchmark(baseline) {
|
|
|
172
255
|
const warmupRuns = baseline.benchmark.warmupRuns;
|
|
173
256
|
const runsPerSample = baseline.benchmark.runsPerSample;
|
|
174
257
|
const sampleDurationsMs = [];
|
|
258
|
+
const totalSampleDurationsMs = [];
|
|
259
|
+
const loadSampleDurationsMs = [];
|
|
260
|
+
const setupSampleDurationsMs = [];
|
|
261
|
+
const edgeBuildSampleDurationsMs = [];
|
|
262
|
+
const indexBuildSampleDurationsMs = [];
|
|
263
|
+
const querySampleDurationsMs = [];
|
|
264
|
+
const clusterBuildSampleDurationsMs = [];
|
|
265
|
+
const peakRssBytesSamples = [];
|
|
266
|
+
const peakHeapUsedBytesSamples = [];
|
|
175
267
|
const benchmarkStartedAt = performance.now();
|
|
176
268
|
let runCounter = 0;
|
|
177
269
|
for (let warmupIndex = 0; warmupIndex < warmupRuns; warmupIndex += 1) {
|
|
178
270
|
const warmupDbPath = path.join(tempRoot, `warmup-${warmupIndex}.sqlite`);
|
|
179
271
|
fs.copyFileSync(seedDbPath, warmupDbPath);
|
|
180
|
-
const warmupResult = await runSingleCluster(warmupDbPath, baseline);
|
|
181
|
-
|
|
182
|
-
assert.ok(warmupResult.edges > baseline.fixture.clusterCount);
|
|
272
|
+
const warmupResult = await runSingleCluster(warmupDbPath, baseline, backend);
|
|
273
|
+
assertBenchmarkShape(warmupResult, baseline, backend);
|
|
183
274
|
}
|
|
184
275
|
while (sampleDurationsMs.length < baseline.benchmark.maxSamples) {
|
|
185
|
-
|
|
276
|
+
let sampleDurationMs = 0;
|
|
277
|
+
let totalSampleDurationMs = 0;
|
|
278
|
+
let loadSampleDurationMs = 0;
|
|
279
|
+
let setupSampleDurationMs = 0;
|
|
280
|
+
let edgeBuildSampleDurationMs = 0;
|
|
281
|
+
let indexBuildSampleDurationMs = 0;
|
|
282
|
+
let querySampleDurationMs = 0;
|
|
283
|
+
let clusterBuildSampleDurationMs = 0;
|
|
284
|
+
let samplePeakRssBytes = 0;
|
|
285
|
+
let samplePeakHeapUsedBytes = 0;
|
|
186
286
|
for (let runIndex = 0; runIndex < runsPerSample; runIndex += 1) {
|
|
187
287
|
const runDbPath = path.join(tempRoot, `run-${runCounter}.sqlite`);
|
|
188
288
|
runCounter += 1;
|
|
189
289
|
fs.copyFileSync(seedDbPath, runDbPath);
|
|
190
|
-
const result = await runSingleCluster(runDbPath, baseline);
|
|
191
|
-
|
|
192
|
-
|
|
290
|
+
const result = await runSingleCluster(runDbPath, baseline, backend);
|
|
291
|
+
assertBenchmarkShape(result, baseline, backend);
|
|
292
|
+
sampleDurationMs += result.durationMs;
|
|
293
|
+
totalSampleDurationMs += result.totalDurationMs;
|
|
294
|
+
loadSampleDurationMs += result.loadMs;
|
|
295
|
+
setupSampleDurationMs += result.setupMs;
|
|
296
|
+
edgeBuildSampleDurationMs += result.edgeBuildMs;
|
|
297
|
+
indexBuildSampleDurationMs += result.indexBuildMs;
|
|
298
|
+
querySampleDurationMs += result.queryMs;
|
|
299
|
+
clusterBuildSampleDurationMs += result.clusterBuildMs;
|
|
300
|
+
samplePeakRssBytes = Math.max(samplePeakRssBytes, result.peakRssBytes);
|
|
301
|
+
samplePeakHeapUsedBytes = Math.max(samplePeakHeapUsedBytes, result.peakHeapUsedBytes);
|
|
193
302
|
}
|
|
194
|
-
sampleDurationsMs.push(
|
|
303
|
+
sampleDurationsMs.push(sampleDurationMs);
|
|
304
|
+
totalSampleDurationsMs.push(totalSampleDurationMs);
|
|
305
|
+
loadSampleDurationsMs.push(loadSampleDurationMs);
|
|
306
|
+
setupSampleDurationsMs.push(setupSampleDurationMs);
|
|
307
|
+
edgeBuildSampleDurationsMs.push(edgeBuildSampleDurationMs);
|
|
308
|
+
indexBuildSampleDurationsMs.push(indexBuildSampleDurationMs);
|
|
309
|
+
querySampleDurationsMs.push(querySampleDurationMs);
|
|
310
|
+
clusterBuildSampleDurationsMs.push(clusterBuildSampleDurationMs);
|
|
311
|
+
peakRssBytesSamples.push(samplePeakRssBytes);
|
|
312
|
+
peakHeapUsedBytesSamples.push(samplePeakHeapUsedBytes);
|
|
195
313
|
const elapsedMs = performance.now() - benchmarkStartedAt;
|
|
196
314
|
if (sampleDurationsMs.length >= baseline.benchmark.minSamples && elapsedMs >= baseline.benchmark.maxTotalMs) {
|
|
197
315
|
break;
|
|
198
316
|
}
|
|
199
317
|
}
|
|
200
318
|
const medianMs = median(sampleDurationsMs);
|
|
319
|
+
const totalMedianMs = median(totalSampleDurationsMs);
|
|
320
|
+
const loadMedianMs = median(loadSampleDurationsMs);
|
|
321
|
+
const setupMedianMs = median(setupSampleDurationsMs);
|
|
322
|
+
const edgeBuildMedianMs = median(edgeBuildSampleDurationsMs);
|
|
323
|
+
const indexBuildMedianMs = median(indexBuildSampleDurationsMs);
|
|
324
|
+
const queryMedianMs = median(querySampleDurationsMs);
|
|
325
|
+
const clusterBuildMedianMs = median(clusterBuildSampleDurationsMs);
|
|
326
|
+
const medianPeakRssBytes = median(peakRssBytesSamples);
|
|
327
|
+
const medianPeakHeapUsedBytes = median(peakHeapUsedBytesSamples);
|
|
201
328
|
const baselineMedianMs = baseline.baseline.fixtureMedianMs > 0 ? baseline.baseline.fixtureMedianMs : medianMs;
|
|
202
329
|
const deltaMs = medianMs - baselineMedianMs;
|
|
203
330
|
const deltaPercent = baselineMedianMs > 0 ? (deltaMs / baselineMedianMs) * 100 : 0;
|
|
@@ -206,8 +333,28 @@ async function measureBenchmark(baseline) {
|
|
|
206
333
|
const projectedDeltaMs = projectedOpenclawMs - projectedBaselineOpenclawMs;
|
|
207
334
|
const projectedDeltaPercent = (projectedDeltaMs / projectedBaselineOpenclawMs) * 100;
|
|
208
335
|
return {
|
|
336
|
+
backend,
|
|
337
|
+
timingBasis: 'cluster-only',
|
|
209
338
|
sampleDurationsMs,
|
|
339
|
+
totalSampleDurationsMs,
|
|
340
|
+
loadSampleDurationsMs,
|
|
341
|
+
setupSampleDurationsMs,
|
|
342
|
+
edgeBuildSampleDurationsMs,
|
|
343
|
+
indexBuildSampleDurationsMs,
|
|
344
|
+
querySampleDurationsMs,
|
|
345
|
+
clusterBuildSampleDurationsMs,
|
|
346
|
+
peakRssBytesSamples,
|
|
347
|
+
peakHeapUsedBytesSamples,
|
|
210
348
|
medianMs,
|
|
349
|
+
totalMedianMs,
|
|
350
|
+
loadMedianMs,
|
|
351
|
+
setupMedianMs,
|
|
352
|
+
edgeBuildMedianMs,
|
|
353
|
+
indexBuildMedianMs,
|
|
354
|
+
queryMedianMs,
|
|
355
|
+
clusterBuildMedianMs,
|
|
356
|
+
medianPeakRssBytes,
|
|
357
|
+
medianPeakHeapUsedBytes,
|
|
211
358
|
baselineMedianMs,
|
|
212
359
|
deltaMs,
|
|
213
360
|
deltaPercent,
|
|
@@ -229,14 +376,30 @@ async function measureBenchmark(baseline) {
|
|
|
229
376
|
function buildSummary(result) {
|
|
230
377
|
const status = result.deltaPercent > result.maxRegressionPercent ? 'FAIL' : 'PASS';
|
|
231
378
|
const sampleList = result.sampleDurationsMs.map((value) => formatDurationMs(value)).join(', ');
|
|
379
|
+
const suggestedBaseline = buildSuggestedBaseline(result);
|
|
380
|
+
const timingLabel = 'Fixture median';
|
|
232
381
|
const bootstrapLine = result.baselineMedianMs === result.medianMs
|
|
233
382
|
? '- Bootstrap mode: using the current fixture median as the provisional baseline'
|
|
234
383
|
: null;
|
|
384
|
+
const suggestedBaselineLine = suggestedBaseline
|
|
385
|
+
? `- Suggested baseline update: ${JSON.stringify(suggestedBaseline)}`
|
|
386
|
+
: null;
|
|
235
387
|
return [
|
|
236
388
|
'## Cluster Performance',
|
|
237
389
|
'',
|
|
390
|
+
`- Backend: ${result.backend}`,
|
|
391
|
+
`- Timing basis: ${result.timingBasis}`,
|
|
238
392
|
`- Status: ${status}`,
|
|
239
|
-
`- Fixture median: ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`,
|
|
393
|
+
`- Fixture median (cluster-only): ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`,
|
|
394
|
+
`- Fixture median (total run): ${formatDurationMs(result.totalMedianMs)}`,
|
|
395
|
+
`- Fixture median load stage: ${formatDurationMs(result.loadMedianMs)}`,
|
|
396
|
+
`- Fixture median setup stage: ${formatDurationMs(result.setupMedianMs)}`,
|
|
397
|
+
`- Fixture median exact edge-build stage: ${formatDurationMs(result.edgeBuildMedianMs)}`,
|
|
398
|
+
`- Fixture median vector index-build stage: ${formatDurationMs(result.indexBuildMedianMs)}`,
|
|
399
|
+
`- Fixture median vector query stage: ${formatDurationMs(result.queryMedianMs)}`,
|
|
400
|
+
`- Fixture median cluster-assembly stage: ${formatDurationMs(result.clusterBuildMedianMs)}`,
|
|
401
|
+
`- Median peak RSS: ${formatBytes(result.medianPeakRssBytes)}`,
|
|
402
|
+
`- Median peak heap used: ${formatBytes(result.medianPeakHeapUsedBytes)}`,
|
|
240
403
|
`- Fixture baseline: ${formatDurationMs(result.baselineMedianMs)}`,
|
|
241
404
|
`- Fixture delta: ${formatDurationMs(result.deltaMs)} (${formatPercent(result.deltaPercent)})`,
|
|
242
405
|
`- Projected openclaw/openclaw duration: ${formatDurationMs(result.projectedOpenclawMs)}`,
|
|
@@ -245,6 +408,7 @@ function buildSummary(result) {
|
|
|
245
408
|
`- Regression threshold: ${formatPercent(result.maxRegressionPercent)}`,
|
|
246
409
|
`- Fixture shape: ${result.threadCount} threads x ${result.sourceKinds.length} source kinds`,
|
|
247
410
|
`- Sample durations: ${sampleList}`,
|
|
411
|
+
suggestedBaselineLine,
|
|
248
412
|
bootstrapLine,
|
|
249
413
|
'',
|
|
250
414
|
]
|
|
@@ -261,6 +425,7 @@ function writeOutput(result, summary, bootstrap) {
|
|
|
261
425
|
status: result.deltaPercent > result.maxRegressionPercent ? 'FAIL' : 'PASS',
|
|
262
426
|
bootstrap,
|
|
263
427
|
summary,
|
|
428
|
+
suggestedBaseline: buildSuggestedBaseline(result),
|
|
264
429
|
result,
|
|
265
430
|
}, null, 2) + '\n');
|
|
266
431
|
}
|
|
@@ -269,10 +434,11 @@ async function main() {
|
|
|
269
434
|
const result = await measureBenchmark(baseline);
|
|
270
435
|
const summary = buildSummary(result);
|
|
271
436
|
const bootstrap = shouldBootstrapBaseline();
|
|
272
|
-
const shouldFail = !bootstrap && result.deltaPercent > result.maxRegressionPercent;
|
|
437
|
+
const shouldFail = !bootstrap && !shouldIgnoreRegressionThreshold() && result.deltaPercent > result.maxRegressionPercent;
|
|
273
438
|
process.stdout.write(`${summary}\n`);
|
|
274
|
-
|
|
275
|
-
|
|
439
|
+
const suggestedBaseline = buildSuggestedBaseline(result);
|
|
440
|
+
if (bootstrap && suggestedBaseline) {
|
|
441
|
+
process.stdout.write(`Suggested baseline update: ${JSON.stringify(suggestedBaseline)}\n`);
|
|
276
442
|
}
|
|
277
443
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
278
444
|
if (summaryPath) {
|