Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/workbench/components/localModeToolbar.tsx
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Badge, Button, Checkbox, ComboboxOpenChangeData, ComboboxOpenEvents, Dropdown, Input, Option, OptionOnSelectData, Select, SelectionEvents, Text, Tooltip } from '@fluentui/react-components';
7
import { Play16Regular, Stop16Regular } from '@fluentui/react-icons';
8
import * as mobx from 'mobx';
9
import * as mobxlite from 'mobx-react-lite';
10
import * as React from 'react';
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
import { InitArgs } from '../initArgs';
13
import { CacheMode, RunnerOptions } from '../stores/runnerOptions';
14
import { SimulationRunsProvider } from '../stores/simulationBaseline';
15
import { SimulationRunner, StateKind } from '../stores/simulationRunner';
16
import { SimulationTestsProvider } from '../stores/simulationTestsProvider';
17
import { useLocalStorageState } from '../stores/storage';
18
import { BaselineJSONPicker } from './baselineJSONPicker';
19
import { CompareAgainstRunPicker } from './compareAgainstRunPicker';
20
import { CurrentRunPicker } from './currentRunPicker';
21
import {
22
createAnnotationFilter,
23
createBaselineChangedFilter,
24
createCacheMissesFilter,
25
createFailuresFilter,
26
createFilterer,
27
createGrepFilter,
28
createRanTestsFilter
29
} from './filterUtils';
30
import { TestFilterer } from './testFilterer';
31
32
/**
33
* Interface for filter parameters used in filtering tests
34
*/
35
interface FilterParams {
36
grep: string;
37
showBaselineJSONChangedOnly: boolean;
38
showFailedOnly: boolean;
39
showWithCacheMissesOnly: boolean;
40
showOnlyRanTests: boolean;
41
showOnlyTestsWithAnnotations: boolean;
42
selectedAnnotations: string[];
43
}
44
45
/**
46
* Type for Fluent UI select option event
47
*/
48
interface SelectOptionEvent {
49
value: string;
50
}
51
52
// Hook for asynchronously applying filters
53
const useAsyncFilter = (
54
filterParams: FilterParams,
55
debounceMs: number,
56
createFilter: (params: FilterParams) => TestFilterer,
57
onFilterChange: (filter: TestFilterer | undefined) => void
58
) => {
59
// Track if component is mounted to prevent state updates after unmount
60
const isMounted = useRef(true);
61
// Store the latest filter parameters
62
const latestFilterParams = useRef(filterParams);
63
// Track if a filter operation is in progress
64
const isFilteringRef = useRef(false);
65
// Track if there's a pending filter operation
66
const hasPendingFilterRef = useRef(false);
67
// Add a state to indicate filtering is in progress (could be used for UI feedback)
68
const [isFiltering, setIsFiltering] = useState(false);
69
70
// Update the latest filter params when they change
71
useEffect(() => {
72
latestFilterParams.current = filterParams;
73
}, [filterParams]);
74
75
// Clean up when component unmounts
76
useEffect(() => {
77
return () => {
78
isMounted.current = false;
79
};
80
}, []);
81
82
// Function to perform the actual filter creation asynchronously
83
const applyFilter = useCallback(() => {
84
if (!isMounted.current) { return; }
85
86
// If already filtering, mark as pending and return
87
if (isFilteringRef.current) {
88
hasPendingFilterRef.current = true;
89
return;
90
}
91
92
isFilteringRef.current = true;
93
setIsFiltering(true);
94
95
// Use setTimeout to move filter creation off the main thread
96
setTimeout(() => {
97
// Create a local copy of the latest filter params to avoid race conditions
98
const params = latestFilterParams.current;
99
100
// Use requestAnimationFrame to schedule filter application for the next frame
101
requestAnimationFrame(() => {
102
try {
103
// Create the filter
104
const newFilter = createFilter(params);
105
106
// Schedule the filter change callback for the next frame to prevent UI blocking
107
requestAnimationFrame(() => {
108
if (isMounted.current) {
109
onFilterChange(newFilter);
110
isFilteringRef.current = false;
111
setIsFiltering(false);
112
113
// If there's a pending filter request, process it
114
if (hasPendingFilterRef.current) {
115
hasPendingFilterRef.current = false;
116
applyFilter();
117
}
118
}
119
});
120
} catch (error) {
121
console.error('Error applying filter:', error);
122
if (isMounted.current) {
123
isFilteringRef.current = false;
124
setIsFiltering(false);
125
126
// If there's a pending filter request, process it
127
if (hasPendingFilterRef.current) {
128
hasPendingFilterRef.current = false;
129
applyFilter();
130
}
131
}
132
}
133
});
134
}, 0); // Use 0ms timeout to defer to the next event loop tick
135
}, [createFilter, onFilterChange]);
136
137
// Set up a debounced effect to apply the filter
138
useEffect(() => {
139
const debounceTimer = setTimeout(applyFilter, debounceMs);
140
return () => clearTimeout(debounceTimer);
141
}, [filterParams, applyFilter, debounceMs]);
142
143
return { isFiltering };
144
};
145
146
type Props = {
147
initArgs: InitArgs | undefined;
148
runner: SimulationRunner;
149
runnerOptions: RunnerOptions;
150
simulationRunsProvider: SimulationRunsProvider;
151
simulationTestsProvider: SimulationTestsProvider;
152
onFiltererChange: (filter: TestFilterer | undefined) => void;
153
};
154
155
export const LocalModeToolbar = mobxlite.observer(
156
({
157
initArgs,
158
runner,
159
runnerOptions,
160
simulationRunsProvider,
161
simulationTestsProvider,
162
onFiltererChange,
163
}: Props) => {
164
165
// filtering
166
const [showBaselineJSONChangedOnly, setShowBaselineJSONChangedOnly] = useLocalStorageState('baselineJSONChangedOnly', undefined, false);
167
const [showFailedOnly, setShowFailedOnly] = useLocalStorageState('showFailedOnly', undefined, false);
168
const [showOnlyRanTests, setShowOnlyRanTests] = useLocalStorageState('showOnlyRanTests', undefined, false);
169
const [showWithCacheMissesOnly, setShowWithCacheMissesOnly] = React.useState(false);
170
const [showOnlyTestsWithAnnotations, setShowOnlyTestsWithAnnotations] = useLocalStorageState('showOnlyTestsWithAnnotations', undefined, false);
171
const [selectedAnnotations, setSelectedAnnotations] = useLocalStorageState<string[]>('selectedAnnotations', undefined, []);
172
173
// Create filter parameters object that will be processed asynchronously
174
const filterParams = useMemo(() => ({
175
grep: runnerOptions.grep.value,
176
showBaselineJSONChangedOnly,
177
showFailedOnly,
178
showWithCacheMissesOnly,
179
showOnlyRanTests,
180
showOnlyTestsWithAnnotations,
181
selectedAnnotations
182
}), [
183
runnerOptions.grep.value,
184
showBaselineJSONChangedOnly,
185
showFailedOnly,
186
showWithCacheMissesOnly,
187
showOnlyRanTests,
188
showOnlyTestsWithAnnotations,
189
selectedAnnotations
190
]);
191
192
// Callback for creating a filter instance - memoized to prevent recreations
193
const createFilter = useCallback((params: FilterParams): TestFilterer => {
194
const predicates = [];
195
196
// Add active filter predicates
197
predicates.push(createGrepFilter(params.grep));
198
199
if (params.showBaselineJSONChangedOnly) {
200
predicates.push(createBaselineChangedFilter());
201
}
202
203
if (params.showFailedOnly) {
204
predicates.push(createFailuresFilter());
205
}
206
207
if (params.showWithCacheMissesOnly) {
208
predicates.push(createCacheMissesFilter());
209
}
210
211
if (params.showOnlyRanTests) {
212
predicates.push(createRanTestsFilter());
213
}
214
215
if (params.showOnlyTestsWithAnnotations && params.selectedAnnotations.length > 0) {
216
predicates.push(createAnnotationFilter(new Set(params.selectedAnnotations)));
217
}
218
219
return createFilterer(predicates);
220
}, []);
221
222
// Memoize the filter change callback
223
const handleFilterChange = useCallback((filter: TestFilterer | undefined) => {
224
onFiltererChange(filter);
225
}, [onFiltererChange]);
226
227
// Use the async filter hook with a longer debounce time (500ms)
228
const { isFiltering } = useAsyncFilter(
229
filterParams,
230
500, // Increased from 300ms to 500ms
231
createFilter,
232
handleFilterChange
233
);
234
235
const isSimulationRunning = runner.state.kind === StateKind.Running;
236
237
const [knownAnnotations, setKnownAnnotations] = React.useState<string[]>([]);
238
239
// Memoize event handlers to prevent recreations on each render
240
const updateKnownAnnotations = useCallback(() => {
241
const newAnnotations = new Set<string>(knownAnnotations);
242
for (const test of simulationTestsProvider.tests) {
243
if (test.runnerStatus) {
244
for (const run of test.runnerStatus.runs) {
245
for (const annotation of run.annotations) {
246
newAnnotations.add(annotation.label);
247
}
248
}
249
}
250
}
251
setKnownAnnotations([...newAnnotations].sort());
252
}, [knownAnnotations, simulationTestsProvider.tests]);
253
254
const handleRunStopButtonClick = useCallback(() => {
255
mobx.runInAction(() => {
256
if (isSimulationRunning) {
257
runner.stopRunning();
258
} else {
259
runner.startRunning({
260
grep: runnerOptions.grep.value,
261
cacheMode: runnerOptions.cacheMode.value,
262
n: parseInt(runnerOptions.n.value),
263
noFetch: runnerOptions.noFetch.value,
264
additionalArgs: runnerOptions.additionalArgs.value,
265
});
266
}
267
});
268
}, [isSimulationRunning, runner, runnerOptions]);
269
270
// Memoize checkbox handlers
271
const handleBaselineJSONChangedOnlyChange = useCallback(() => {
272
setShowBaselineJSONChangedOnly(!showBaselineJSONChangedOnly);
273
}, [showBaselineJSONChangedOnly, setShowBaselineJSONChangedOnly]);
274
275
const handleShowFailedOnlyChange = useCallback(() => {
276
setShowFailedOnly(!showFailedOnly);
277
}, [showFailedOnly, setShowFailedOnly]);
278
279
const handleShowWithCacheMissesOnlyChange = useCallback(() => {
280
setShowWithCacheMissesOnly(!showWithCacheMissesOnly);
281
}, [showWithCacheMissesOnly]);
282
283
const handleShowOnlyRanTestsChange = useCallback(() => {
284
setShowOnlyRanTests(!showOnlyRanTests);
285
}, [showOnlyRanTests, setShowOnlyRanTests]);
286
287
const handleShowOnlyTestsWithAnnotationsChange = useCallback(() => {
288
setShowOnlyTestsWithAnnotations(!showOnlyTestsWithAnnotations);
289
}, [showOnlyTestsWithAnnotations, setShowOnlyTestsWithAnnotations]);
290
291
const handleOptionSelect = useCallback((_e: SelectionEvents, o: OptionOnSelectData) => {
292
setSelectedAnnotations(o.selectedOptions);
293
setShowOnlyTestsWithAnnotations(o.selectedOptions.length > 0);
294
}, [setSelectedAnnotations, setShowOnlyTestsWithAnnotations]);
295
296
const handleOpenChange = useCallback((_e: ComboboxOpenEvents, o: ComboboxOpenChangeData) => {
297
if (o.open) {
298
updateKnownAnnotations();
299
}
300
}, [updateKnownAnnotations]);
301
302
const handleGrepChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {
303
mobx.runInAction(() => {
304
runnerOptions.grep.value = (e.target as HTMLInputElement).value;
305
});
306
}, [runnerOptions.grep]);
307
308
const handleNRunsChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {
309
runnerOptions.n.value = (e.target as HTMLInputElement).value;
310
}, [runnerOptions.n]);
311
312
const handleNoFetchChange = useCallback(() => {
313
mobx.runInAction(() => {
314
runnerOptions.noFetch.value = !runnerOptions.noFetch.value;
315
});
316
}, [runnerOptions.noFetch]);
317
318
const handleExtraArgsChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {
319
mobx.runInAction(() => {
320
runnerOptions.additionalArgs.value = (e.target as HTMLInputElement).value;
321
});
322
}, [runnerOptions.additionalArgs]);
323
324
const handleUpdateBaselineJSON = useCallback(() => {
325
simulationTestsProvider.baselineJSONProvider.updateRootBaselineJSON();
326
}, [simulationTestsProvider.baselineJSONProvider]);
327
328
const handleRunFromDiskChange = useCallback((selected: string | undefined) => {
329
const name = selected ?? '';
330
runner.setSelectedRunFromDisk(name);
331
}, [runner]);
332
333
const handleCacheModeChange = useCallback((_e: React.FormEvent<HTMLSelectElement>, option: SelectOptionEvent) => {
334
runnerOptions.cacheMode.value = option.value as CacheMode;
335
}, [runnerOptions.cacheMode]);
336
337
return (
338
<div
339
className='toolbar'
340
style={{
341
display: 'flex',
342
flexDirection: 'column',
343
justifyContent: 'space-between',
344
}}
345
>
346
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
347
<div style={{ display: 'flex', alignItems: 'center' }}>
348
<CurrentRunPicker
349
simulationRunsProvider={simulationRunsProvider}
350
disabled={isSimulationRunning}
351
onChange={handleRunFromDiskChange}
352
outputFolderName={runner.selectedRun}
353
/>
354
<CompareAgainstRunPicker
355
simulationRunsProvider={simulationRunsProvider}
356
/>
357
<BaselineJSONPicker testsProvider={simulationTestsProvider} />
358
</div>
359
</div>
360
<div style={{ height: '8px' }} />
361
<div>
362
<div>Configure new run</div>
363
<div style={{ display: 'flex', gap: '10px' }}>
364
<Input
365
id='grep'
366
size='small'
367
placeholder='grep'
368
title='Filter by test name'
369
value={runnerOptions.grep.value}
370
onChange={handleGrepChange}
371
style={{ width: '300px', maxWidth: '25vw' }}
372
/>
373
<Input
374
id='nRuns'
375
size='small'
376
style={{ width: '40px' }}
377
placeholder='N'
378
title='Specify number of runs per each test'
379
value={runnerOptions.n.value}
380
onChange={handleNRunsChange}
381
/>
382
<Select onChange={handleCacheModeChange} defaultValue={runnerOptions.cacheMode.value}>
383
<option value={CacheMode.Default} title='Use cache if available'>Use Cache</option>
384
<option value={CacheMode.Disable} title='Do not use cache'>No Cache</option>
385
<option value={CacheMode.Require} title='Use cache, fail if not available'>Require Cache</option>
386
</Select>
387
<Tooltip relationship='label' content={'Do not send requests to the model endpoint (uses cache but doesn\'t write to it) (useful to make sure prompts are unchanged by observing cache misses)'}>
388
<Checkbox
389
label='No fetch'
390
defaultChecked={runnerOptions.noFetch.value}
391
onChange={handleNoFetchChange}
392
/>
393
</Tooltip>
394
<Input
395
id='extraArgs'
396
size='small'
397
style={{ width: '300px' }}
398
placeholder='Extra args, e.g., --parallelism=10 --require-cache'
399
value={runnerOptions.additionalArgs.value}
400
onInput={handleExtraArgsChange}
401
/>
402
<Button
403
size='small' appearance={isSimulationRunning ? 'secondary' : 'primary'}
404
icon={isSimulationRunning ? <Stop16Regular /> : <Play16Regular />}
405
iconPosition='before'
406
onClick={handleRunStopButtonClick}
407
style={{ width: '69px' }}
408
>
409
{isSimulationRunning ? 'Stop' : 'Run'}
410
</Button>
411
{
412
runner.selectedRun !== '' && runner.state.kind !== StateKind.Running &&
413
<Button size='small' onClick={handleUpdateBaselineJSON}> Update baseline.json </Button>
414
}
415
</div>
416
</div>
417
<div style={{ height: '8px' }} />
418
<div>
419
<Tooltip content={'Only modify tests displayed. Tests will be run even if not in list below'} relationship={'label'}>
420
<Text>View Filters {isFiltering && '(Filtering...)'}</Text>
421
</Tooltip>
422
<div style={{ display: 'flex', alignItems: 'center' }}>
423
<Checkbox
424
className='showBaselineJSONChanged'
425
label='Show baseline.json changed only'
426
checked={showBaselineJSONChangedOnly}
427
onChange={handleBaselineJSONChangedOnlyChange}
428
/>
429
<Checkbox
430
className='showFailedOnly'
431
label='Show with failures only'
432
checked={showFailedOnly}
433
onChange={handleShowFailedOnlyChange}
434
/>
435
<Checkbox
436
label='Show with cache misses only'
437
checked={showWithCacheMissesOnly}
438
onChange={handleShowWithCacheMissesOnlyChange}
439
/>
440
<Checkbox
441
label='Show ran tests only'
442
checked={showOnlyRanTests}
443
onChange={handleShowOnlyRanTestsChange}
444
/>
445
<Checkbox
446
label='Show with annotations only:'
447
checked={showOnlyTestsWithAnnotations}
448
onChange={handleShowOnlyTestsWithAnnotationsChange}
449
/>
450
<Dropdown
451
multiselect
452
size='small'
453
placeholder='Select annotations'
454
defaultValue={selectedAnnotations.length ? selectedAnnotations.join(', ') : undefined}
455
defaultSelectedOptions={selectedAnnotations}
456
onOptionSelect={handleOptionSelect}
457
onOpenChange={handleOpenChange}
458
>
459
{knownAnnotations.map((option) => (
460
<Option key={option} text={option}>
461
<Badge key={option} shape='square' appearance='outline' size='small'>{option}</Badge>
462
</Option>
463
))}
464
</Dropdown>
465
</div>
466
</div>
467
</div >
468
);
469
}
470
);
471
472