Skip to content

Commit b1dc862

Browse files
authored
Merge pull request #2682 from github/charisk/batch-automodeling
Batch automodeling
2 parents bd6a7b2 + 4b0f599 commit b1dc862

File tree

10 files changed

+143
-51
lines changed

10 files changed

+143
-51
lines changed

extensions/ql-vscode/src/common/interface-types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,11 @@ interface GenerateExternalApiFromLlmMessage {
554554
modeledMethods: Record<string, ModeledMethod>;
555555
}
556556

557+
interface StopGeneratingExternalApiFromLlmMessage {
558+
t: "stopGeneratingExternalApiFromLlm";
559+
dependencyName: string;
560+
}
561+
557562
interface ModelDependencyMessage {
558563
t: "modelDependency";
559564
}
@@ -575,4 +580,5 @@ export type FromDataExtensionsEditorMessage =
575580
| SaveModeledMethods
576581
| GenerateExternalApiMessage
577582
| GenerateExternalApiFromLlmMessage
583+
| StopGeneratingExternalApiFromLlmMessage
578584
| ModelDependencyMessage;

extensions/ql-vscode/src/data-extensions-editor/auto-model-codeml-queries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ type AutoModelQueriesOptions = {
160160
queryStorageDir: string;
161161

162162
progress: ProgressCallback;
163+
cancellationTokenSource: CancellationTokenSource;
163164
};
164165

165166
export type AutoModelQueriesResult = {
@@ -174,12 +175,11 @@ export async function runAutoModelQueries({
174175
databaseItem,
175176
queryStorageDir,
176177
progress,
178+
cancellationTokenSource,
177179
}: AutoModelQueriesOptions): Promise<AutoModelQueriesResult | undefined> {
178180
// maxStep for this part is 1500
179181
const maxStep = 1500;
180182

181-
const cancellationTokenSource = new CancellationTokenSource();
182-
183183
const qlpack = await qlpackOfDatabase(cliServer, databaseItem);
184184

185185
// CodeQL needs to have access to the database to be able to retrieve the

extensions/ql-vscode/src/data-extensions-editor/auto-model-v2.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import { ExternalApiUsage, MethodSignature } from "./external-api-usage";
88
import { ModeledMethod } from "./modeled-method";
99
import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
1010

11-
// Soft limit on the number of candidates to send to the model.
12-
// Note that the model may return fewer than this number of candidates.
13-
const candidateLimit = 20;
1411
/**
1512
* Return the candidates that the model should be run on. This includes limiting the number of
1613
* candidates to the candidate limit and filtering out anything that is already modeled and respecting
@@ -41,11 +38,6 @@ export function getCandidates(
4138
type: "none",
4239
};
4340

44-
// If we have reached the max number of candidates then stop
45-
if (candidates.length >= candidateLimit) {
46-
break;
47-
}
48-
4941
// Anything that is modeled is not a candidate
5042
if (modeledMethod.type !== "none") {
5143
continue;

extensions/ql-vscode/src/data-extensions-editor/auto-modeler.ts

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,63 +15,134 @@ import { CodeQLCliServer } from "../codeql-cli/cli";
1515
import { QueryRunner } from "../query-server";
1616
import { DatabaseItem } from "../databases/local-databases";
1717
import { Mode } from "./shared/mode";
18+
import { CancellationTokenSource } from "vscode";
19+
20+
// Limit the number of candidates we send to the model in each request
21+
// to avoid long requests.
22+
// Note that the model may return fewer than this number of candidates.
23+
const candidateBatchSize = 20;
1824

1925
export class AutoModeler {
26+
private readonly jobs: Map<string, CancellationTokenSource>;
27+
2028
constructor(
2129
private readonly app: App,
2230
private readonly cliServer: CodeQLCliServer,
2331
private readonly queryRunner: QueryRunner,
2432
private readonly queryStorageDir: string,
2533
private readonly databaseItem: DatabaseItem,
34+
private readonly setInProgressMethods: (
35+
inProgressMethods: string[],
36+
) => Promise<void>,
2637
private readonly addModeledMethods: (
2738
modeledMethods: Record<string, ModeledMethod>,
2839
) => Promise<void>,
29-
) {}
40+
) {
41+
this.jobs = new Map<string, CancellationTokenSource>();
42+
}
3043

3144
public async startModeling(
3245
dependency: string,
3346
externalApiUsages: ExternalApiUsage[],
3447
modeledMethods: Record<string, ModeledMethod>,
3548
mode: Mode,
3649
): Promise<void> {
37-
await this.modelDependency(
38-
dependency,
39-
externalApiUsages,
40-
modeledMethods,
41-
mode,
42-
);
50+
if (this.jobs.has(dependency)) {
51+
return;
52+
}
53+
54+
const cancellationTokenSource = new CancellationTokenSource();
55+
this.jobs.set(dependency, cancellationTokenSource);
56+
57+
try {
58+
await this.modelDependency(
59+
dependency,
60+
externalApiUsages,
61+
modeledMethods,
62+
mode,
63+
cancellationTokenSource,
64+
);
65+
} finally {
66+
this.jobs.delete(dependency);
67+
}
68+
}
69+
70+
public async stopModeling(dependency: string): Promise<void> {
71+
void extLogger.log(`Stopping modeling for dependency ${dependency}`);
72+
const cancellationTokenSource = this.jobs.get(dependency);
73+
if (cancellationTokenSource) {
74+
cancellationTokenSource.cancel();
75+
}
76+
}
77+
78+
public async stopAllModeling(): Promise<void> {
79+
for (const cancellationTokenSource of this.jobs.values()) {
80+
cancellationTokenSource.cancel();
81+
}
4382
}
4483

4584
private async modelDependency(
4685
dependency: string,
4786
externalApiUsages: ExternalApiUsage[],
4887
modeledMethods: Record<string, ModeledMethod>,
4988
mode: Mode,
89+
cancellationTokenSource: CancellationTokenSource,
5090
): Promise<void> {
5191
void extLogger.log(`Modeling dependency ${dependency}`);
5292
await withProgress(async (progress) => {
5393
const maxStep = 3000;
5494

55-
progress({
56-
step: 0,
57-
maxStep,
58-
message: "Retrieving usages",
59-
});
60-
6195
// Fetch the candidates to send to the model
62-
const candidateMethods = getCandidates(
96+
const allCandidateMethods = getCandidates(
6397
mode,
6498
externalApiUsages,
6599
modeledMethods,
66100
);
67101

68102
// If there are no candidates, there is nothing to model and we just return
69-
if (candidateMethods.length === 0) {
103+
if (allCandidateMethods.length === 0) {
70104
void extLogger.log("No candidates to model. Stopping.");
71105
return;
72106
}
73107

74-
await this.modelCandidates(candidateMethods, mode, progress, maxStep);
108+
// Find number of slices to make
109+
const batchNumber = Math.ceil(
110+
allCandidateMethods.length / candidateBatchSize,
111+
);
112+
try {
113+
for (let i = 0; i < batchNumber; i++) {
114+
if (cancellationTokenSource.token.isCancellationRequested) {
115+
break;
116+
}
117+
118+
const start = i * candidateBatchSize;
119+
const end = start + candidateBatchSize;
120+
const candidatesToProcess = allCandidateMethods.slice(start, end);
121+
122+
await this.setInProgressMethods(
123+
candidatesToProcess.map((c) => c.signature),
124+
);
125+
126+
progress({
127+
step: 1800 + i * 100,
128+
maxStep,
129+
message: `Automodeling candidates, batch ${
130+
i + 1
131+
} of ${batchNumber}`,
132+
});
133+
134+
await this.modelCandidates(
135+
candidatesToProcess,
136+
mode,
137+
progress,
138+
maxStep,
139+
cancellationTokenSource,
140+
);
141+
}
142+
} finally {
143+
// Clear out in progress methods
144+
await this.setInProgressMethods([]);
145+
}
75146
});
76147
}
77148

@@ -80,6 +151,7 @@ export class AutoModeler {
80151
mode: Mode,
81152
progress: ProgressCallback,
82153
maxStep: number,
154+
cancellationTokenSource: CancellationTokenSource,
83155
): Promise<void> {
84156
const usages = await runAutoModelQueries({
85157
mode,
@@ -89,6 +161,7 @@ export class AutoModeler {
89161
queryStorageDir: this.queryStorageDir,
90162
databaseItem: this.databaseItem,
91163
progress: (update) => progress({ ...update, maxStep }),
164+
cancellationTokenSource,
92165
});
93166
if (!usages) {
94167
return;

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ export class DataExtensionsEditorView extends AbstractWebview<
8383
queryRunner,
8484
queryStorageDir,
8585
databaseItem,
86+
async (inProgressMethods) => {
87+
await this.postMessage({
88+
t: "setInProgressMethods",
89+
inProgressMethods,
90+
});
91+
},
8692
async (modeledMethods) => {
8793
await this.postMessage({ t: "addModeledMethods", modeledMethods });
8894
},
@@ -182,6 +188,9 @@ export class DataExtensionsEditorView extends AbstractWebview<
182188
);
183189
}
184190
break;
191+
case "stopGeneratingExternalApiFromLlm":
192+
await this.autoModeler.stopModeling(msg.dependencyName);
193+
break;
185194
case "modelDependency":
186195
await this.modelDependency();
187196
break;

extensions/ql-vscode/src/view/data-extensions-editor/DataExtensionsEditor.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ export function DataExtensionsEditor({
241241
[],
242242
);
243243

244+
const onStopGenerateFromLlmClick = useCallback((dependencyName: string) => {
245+
vscode.postMessage({
246+
t: "stopGeneratingExternalApiFromLlm",
247+
dependencyName,
248+
});
249+
}, []);
250+
244251
const onOpenDatabaseClick = useCallback(() => {
245252
vscode.postMessage({
246253
t: "openDatabase",
@@ -345,6 +352,7 @@ export function DataExtensionsEditor({
345352
onChange={onChange}
346353
onSaveModelClick={onSaveModelClick}
347354
onGenerateFromLlmClick={onGenerateFromLlmClick}
355+
onStopGenerateFromLlmClick={onStopGenerateFromLlmClick}
348356
onGenerateFromSourceClick={onGenerateFromSourceClick}
349357
onModelDependencyClick={onModelDependencyClick}
350358
/>

extensions/ql-vscode/src/view/data-extensions-editor/LibraryRow.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ type Props = {
8989
externalApiUsages: ExternalApiUsage[],
9090
modeledMethods: Record<string, ModeledMethod>,
9191
) => void;
92+
onStopGenerateFromLlmClick: (dependencyName: string) => void;
9293
onGenerateFromSourceClick: () => void;
9394
onModelDependencyClick: () => void;
9495
};
@@ -105,6 +106,7 @@ export const LibraryRow = ({
105106
onChange,
106107
onSaveModelClick,
107108
onGenerateFromLlmClick,
109+
onStopGenerateFromLlmClick,
108110
onGenerateFromSourceClick,
109111
onModelDependencyClick,
110112
}: Props) => {
@@ -127,6 +129,15 @@ export const LibraryRow = ({
127129
[title, externalApiUsages, modeledMethods, onGenerateFromLlmClick],
128130
);
129131

132+
const handleStopModelWithAI = useCallback(
133+
async (e: React.MouseEvent) => {
134+
onStopGenerateFromLlmClick(title);
135+
e.stopPropagation();
136+
e.preventDefault();
137+
},
138+
[title, onStopGenerateFromLlmClick],
139+
);
140+
130141
const handleModelFromSource = useCallback(
131142
async (e: React.MouseEvent) => {
132143
onGenerateFromSourceClick();
@@ -167,6 +178,12 @@ export const LibraryRow = ({
167178
);
168179
}, [externalApiUsages, modifiedSignatures]);
169180

181+
const canStopAutoModeling = useMemo(() => {
182+
return externalApiUsages.some((externalApiUsage) =>
183+
inProgressSignatures.has(externalApiUsage.signature),
184+
);
185+
}, [externalApiUsages, inProgressSignatures]);
186+
170187
return (
171188
<LibraryContainer>
172189
<TitleContainer onClick={toggleExpanded} aria-expanded={isExpanded}>
@@ -185,12 +202,18 @@ export const LibraryRow = ({
185202
</ModeledPercentage>
186203
{hasUnsavedChanges ? <VSCodeTag>UNSAVED</VSCodeTag> : null}
187204
</NameContainer>
188-
{viewState.showLlmButton && (
205+
{viewState.showLlmButton && !canStopAutoModeling && (
189206
<VSCodeButton appearance="icon" onClick={handleModelWithAI}>
190207
<Codicon name="lightbulb-autofix" label="Model with AI" />
191208
&nbsp;Model with AI
192209
</VSCodeButton>
193210
)}
211+
{viewState.showLlmButton && canStopAutoModeling && (
212+
<VSCodeButton appearance="icon" onClick={handleStopModelWithAI}>
213+
<Codicon name="debug-stop" label="Stop model with AI" />
214+
&nbsp;Stop
215+
</VSCodeButton>
216+
)}
194217
{viewState.mode === Mode.Application && (
195218
<VSCodeButton appearance="icon" onClick={handleModelFromSource}>
196219
<Codicon name="code" label="Model from source" />

extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodsList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type Props = {
3131
externalApiUsages: ExternalApiUsage[],
3232
modeledMethods: Record<string, ModeledMethod>,
3333
) => void;
34+
onStopGenerateFromLlmClick: (dependencyName: string) => void;
3435
onGenerateFromSourceClick: () => void;
3536
onModelDependencyClick: () => void;
3637
};
@@ -49,6 +50,7 @@ export const ModeledMethodsList = ({
4950
onChange,
5051
onSaveModelClick,
5152
onGenerateFromLlmClick,
53+
onStopGenerateFromLlmClick,
5254
onGenerateFromSourceClick,
5355
onModelDependencyClick,
5456
}: Props) => {
@@ -93,6 +95,7 @@ export const ModeledMethodsList = ({
9395
onChange={onChange}
9496
onSaveModelClick={onSaveModelClick}
9597
onGenerateFromLlmClick={onGenerateFromLlmClick}
98+
onStopGenerateFromLlmClick={onStopGenerateFromLlmClick}
9699
onGenerateFromSourceClick={onGenerateFromSourceClick}
97100
onModelDependencyClick={onModelDependencyClick}
98101
/>

extensions/ql-vscode/test/unit-tests/data-extensions-editor/auto-model-v2.test.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -165,28 +165,4 @@ describe("getCandidates", () => {
165165
);
166166
expect(candidates.length).toEqual(1);
167167
});
168-
169-
it("respects the limit", () => {
170-
const externalApiUsages: ExternalApiUsage[] = [];
171-
for (let i = 0; i < 30; i++) {
172-
externalApiUsages.push({
173-
library: "my.jar",
174-
signature: `org.my.A#x${i}()`,
175-
packageName: "org.my",
176-
typeName: "A",
177-
methodName: `x${i}`,
178-
methodParameters: "()",
179-
supported: false,
180-
supportedType: "none",
181-
usages: [],
182-
});
183-
}
184-
const modeledMethods = {};
185-
const candidates = getCandidates(
186-
Mode.Application,
187-
externalApiUsages,
188-
modeledMethods,
189-
);
190-
expect(candidates.length).toEqual(20);
191-
});
192168
});

0 commit comments

Comments
 (0)