Path: blob/main/extensions/copilot/test/simulation/workbench/components/nesExternalModeToolbar.tsx
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Button, Checkbox, Input, Select, Text, Tooltip } from '@fluentui/react-components';6import { Play16Regular, Stop16Regular } from '@fluentui/react-icons';7import * as mobx from 'mobx';8import * as mobxlite from 'mobx-react-lite';9import * as React from 'react';10import { useCallback, useEffect, useMemo, useRef, useState } from 'react';11import { NesExternalOptions } from '../stores/nesExternalOptions';12import { CacheMode, RunnerOptions } from '../stores/runnerOptions';13import { SimulationRunsProvider } from '../stores/simulationBaseline';14import { SimulationRunner, StateKind } from '../stores/simulationRunner';15import { useLocalStorageState } from '../stores/storage';16import { CompareAgainstRunPicker } from './compareAgainstRunPicker';17import { CurrentRunPicker } from './currentRunPicker';18import {19createCacheMissesFilter,20createFailuresFilter,21createFilterer,22createGrepFilter,23createRanTestsFilter,24} from './filterUtils';25import { TestFilterer } from './testFilterer';2627interface FilterParams {28grep: string;29showFailedOnly: boolean;30showWithCacheMissesOnly: boolean;31showOnlyRanTests: boolean;32}3334/** Adapted from localModeToolbar's useAsyncFilter */35const useAsyncFilter = (36filterParams: FilterParams,37debounceMs: number,38createFilter: (params: FilterParams) => TestFilterer,39onFilterChange: (filter: TestFilterer | undefined) => void40) => {41const isMounted = useRef(true);42const latestFilterParams = useRef(filterParams);43const isFilteringRef = useRef(false);44const hasPendingFilterRef = useRef(false);45const [isFiltering, setIsFiltering] = useState(false);4647useEffect(() => {48latestFilterParams.current = filterParams;49}, [filterParams]);5051useEffect(() => {52return () => { isMounted.current = false; };53}, []);5455const applyFilter = useCallback(() => {56if (!isMounted.current) { return; }57if (isFilteringRef.current) {58hasPendingFilterRef.current = true;59return;60}61isFilteringRef.current = true;62setIsFiltering(true);6364setTimeout(() => {65const params = latestFilterParams.current;66requestAnimationFrame(() => {67try {68const newFilter = createFilter(params);69requestAnimationFrame(() => {70if (isMounted.current) {71onFilterChange(newFilter);72isFilteringRef.current = false;73setIsFiltering(false);74if (hasPendingFilterRef.current) {75hasPendingFilterRef.current = false;76applyFilter();77}78}79});80} catch (error) {81console.error('Error applying filter:', error);82if (isMounted.current) {83isFilteringRef.current = false;84setIsFiltering(false);85if (hasPendingFilterRef.current) {86hasPendingFilterRef.current = false;87applyFilter();88}89}90}91});92}, 0);93}, [createFilter, onFilterChange]);9495useEffect(() => {96const debounceTimer = setTimeout(applyFilter, debounceMs);97return () => clearTimeout(debounceTimer);98}, [filterParams, applyFilter, debounceMs]);99100return { isFiltering };101};102103type SelectOptionEvent = { value: string };104105type Props = {106runner: SimulationRunner;107runnerOptions: RunnerOptions;108nesExternalOptions: NesExternalOptions;109simulationRunsProvider: SimulationRunsProvider;110onFiltererChange: (filter: TestFilterer | undefined) => void;111};112113export const NesExternalModeToolbar = mobxlite.observer(114({115runner,116runnerOptions,117nesExternalOptions,118simulationRunsProvider,119onFiltererChange,120}: Props) => {121122const [showFailedOnly, setShowFailedOnly] = useLocalStorageState('nesShowFailedOnly', undefined, false);123const [showOnlyRanTests, setShowOnlyRanTests] = useLocalStorageState('nesShowOnlyRanTests', undefined, false);124const [showWithCacheMissesOnly, setShowWithCacheMissesOnly] = useState(false);125126const filterParams = useMemo(() => ({127grep: runnerOptions.grep.value,128showFailedOnly,129showWithCacheMissesOnly,130showOnlyRanTests,131}), [132runnerOptions.grep.value,133showFailedOnly,134showWithCacheMissesOnly,135showOnlyRanTests,136]);137138const createFilter = useCallback((params: FilterParams): TestFilterer => {139const predicates = [];140predicates.push(createGrepFilter(params.grep));141if (params.showFailedOnly) {142predicates.push(createFailuresFilter());143}144if (params.showWithCacheMissesOnly) {145predicates.push(createCacheMissesFilter());146}147if (params.showOnlyRanTests) {148predicates.push(createRanTestsFilter());149}150return createFilterer(predicates);151}, []);152153const handleFilterChange = useCallback((filter: TestFilterer | undefined) => {154onFiltererChange(filter);155}, [onFiltererChange]);156157const { isFiltering } = useAsyncFilter(filterParams, 500, createFilter, handleFilterChange);158159const isSimulationRunning = runner.state.kind === StateKind.Running;160161const handleRunStopButtonClick = useCallback(() => {162mobx.runInAction(() => {163if (isSimulationRunning) {164runner.stopRunning();165} else {166runner.startRunning({167grep: runnerOptions.grep.value,168cacheMode: runnerOptions.cacheMode.value,169n: parseInt(runnerOptions.n.value),170noFetch: runnerOptions.noFetch.value,171additionalArgs: runnerOptions.additionalArgs.value,172nesExternalScenariosPath: nesExternalOptions.externalScenariosPath.value,173});174}175});176}, [isSimulationRunning, runner, runnerOptions, nesExternalOptions]);177178const handleScenariosPathChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {179mobx.runInAction(() => {180nesExternalOptions.externalScenariosPath.value = (e.target as HTMLInputElement).value;181});182}, [nesExternalOptions]);183184const handleGrepChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {185mobx.runInAction(() => {186runnerOptions.grep.value = (e.target as HTMLInputElement).value;187});188}, [runnerOptions.grep]);189190const handleNRunsChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {191runnerOptions.n.value = (e.target as HTMLInputElement).value;192}, [runnerOptions.n]);193194const handleNoFetchChange = useCallback(() => {195mobx.runInAction(() => {196runnerOptions.noFetch.value = !runnerOptions.noFetch.value;197});198}, [runnerOptions.noFetch]);199200const handleExtraArgsChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {201mobx.runInAction(() => {202runnerOptions.additionalArgs.value = (e.target as HTMLInputElement).value;203});204}, [runnerOptions.additionalArgs]);205206const handleRunFromDiskChange = useCallback((selected: string | undefined) => {207const name = selected ?? '';208runner.setSelectedRunFromDisk(name);209}, [runner]);210211const handleCacheModeChange = useCallback((_e: React.FormEvent<HTMLSelectElement>, option: SelectOptionEvent) => {212runnerOptions.cacheMode.value = option.value as CacheMode;213}, [runnerOptions.cacheMode]);214215const hasValidScenariosPath = nesExternalOptions.externalScenariosPath.value.length > 0;216217return (218<div219className='toolbar'220style={{221display: 'flex',222flexDirection: 'column',223justifyContent: 'space-between',224}}225>226<div style={{ display: 'flex', justifyContent: 'space-between' }}>227<div style={{ display: 'flex', alignItems: 'center' }}>228<CurrentRunPicker229simulationRunsProvider={simulationRunsProvider}230disabled={isSimulationRunning}231onChange={handleRunFromDiskChange}232outputFolderName={runner.selectedRun}233/>234<CompareAgainstRunPicker235simulationRunsProvider={simulationRunsProvider}236/>237</div>238</div>239<div style={{ height: '8px' }} />240<div>241<div>NES External Scenarios</div>242<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>243<Input244id='nesExternalScenariosPath'245size='small'246placeholder='Path to external scenarios, e.g., ../eval/simulation/nes'247title='Path to directory containing NES external scenario recordings'248value={nesExternalOptions.externalScenariosPath.value}249onChange={handleScenariosPathChange}250style={{ width: '400px', maxWidth: '30vw' }}251/>252</div>253</div>254<div style={{ height: '8px' }} />255<div>256<div>Configure new run</div>257<div style={{ display: 'flex', gap: '10px' }}>258<Input259id='grep'260size='small'261placeholder='grep'262title='Filter by test name'263value={runnerOptions.grep.value}264onChange={handleGrepChange}265style={{ width: '300px', maxWidth: '25vw' }}266/>267<Input268id='nRuns'269size='small'270style={{ width: '40px' }}271placeholder='N'272title='Specify number of runs per each test'273value={runnerOptions.n.value}274onChange={handleNRunsChange}275/>276<Select onChange={handleCacheModeChange} defaultValue={runnerOptions.cacheMode.value}>277<option value={CacheMode.Default} title='Use cache if available'>Use Cache</option>278<option value={CacheMode.Disable} title='Do not use cache'>No Cache</option>279<option value={CacheMode.Require} title='Use cache, fail if not available'>Require Cache</option>280</Select>281<Tooltip relationship='label' content={'Do not send requests to the model endpoint (uses cache but doesn\'t write to it)'}>282<Checkbox283label='No fetch'284defaultChecked={runnerOptions.noFetch.value}285onChange={handleNoFetchChange}286/>287</Tooltip>288<Input289id='extraArgs'290size='small'291style={{ width: '300px' }}292placeholder='Extra args, e.g., --parallelism=10 --require-cache'293value={runnerOptions.additionalArgs.value}294onInput={handleExtraArgsChange}295/>296<Tooltip relationship='label' content={!hasValidScenariosPath ? 'Set the external scenarios path first' : ''}>297<Button298size='small' appearance={isSimulationRunning ? 'secondary' : 'primary'}299icon={isSimulationRunning ? <Stop16Regular /> : <Play16Regular />}300iconPosition='before'301onClick={handleRunStopButtonClick}302disabled={!hasValidScenariosPath && !isSimulationRunning}303style={{ width: '69px' }}304>305{isSimulationRunning ? 'Stop' : 'Run'}306</Button>307</Tooltip>308</div>309</div>310<div style={{ height: '8px' }} />311<div>312<Tooltip content={'Only modify tests displayed. Tests will be run even if not in list below'} relationship={'label'}>313<Text>View Filters {isFiltering && '(Filtering...)'}</Text>314</Tooltip>315<div style={{ display: 'flex', alignItems: 'center' }}>316<Checkbox317className='showFailedOnly'318label='Show with failures only'319checked={showFailedOnly}320onChange={() => setShowFailedOnly(!showFailedOnly)}321/>322<Checkbox323label='Show with cache misses only'324checked={showWithCacheMissesOnly}325onChange={() => setShowWithCacheMissesOnly(!showWithCacheMissesOnly)}326/>327<Checkbox328label='Show ran tests only'329checked={showOnlyRanTests}330onChange={() => setShowOnlyRanTests(!showOnlyRanTests)}331/>332</div>333</div>334</div>335);336}337);338339340