addons/isl/src/debug/DebugToolsMenu.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ReactNode} from 'react';
b69ab319import type {ExclusiveOr} from 'shared/typeUtils';
b69ab3110import type {Heartbeat} from '../heartbeat';
b69ab3111
b69ab3112import * as stylex from '@stylexjs/stylex';
b69ab3113import {Badge} from 'isl-components/Badge';
b69ab3114import {Banner, BannerKind} from 'isl-components/Banner';
b69ab3115import {Button} from 'isl-components/Button';
b69ab3116import {Checkbox} from 'isl-components/Checkbox';
b69ab3117import {InlineErrorBadge} from 'isl-components/ErrorNotice';
b69ab3118import {Icon} from 'isl-components/Icon';
b69ab3119import {Subtle} from 'isl-components/Subtle';
b69ab3120import {Tooltip} from 'isl-components/Tooltip';
b69ab3121import {atom, useAtom, useAtomValue} from 'jotai';
b69ab3122import {useCallback, useEffect, useState} from 'react';
b69ab3123import {colors} from '../../../components/theme/tokens.stylex';
b69ab3124import serverApi, {debugLogMessageTraffic} from '../ClientToServerAPI';
b69ab3125import {Column, Row} from '../ComponentUtils';
b69ab3126import {DropdownField, DropdownFields} from '../DropdownFields';
b69ab3127import {enableReactTools, enableReduxTools} from '../atoms/debugToolAtoms';
b69ab3128import {holdingCtrlAtom} from '../atoms/keyboardAtoms';
b69ab3129import {DagCommitInfo} from '../dag/dagCommitInfo';
b69ab3130import {useHeartbeat} from '../heartbeat';
b69ab3131import {t, T} from '../i18n';
b69ab3132import {atomWithOnChange} from '../jotaiUtils';
b69ab3133import {NopOperation} from '../operations/NopOperation';
b69ab3134import {useRunOperation} from '../operationsState';
b69ab3135import platform from '../platform';
b69ab3136import {dagWithPreviews} from '../previews';
b69ab3137import {RelativeDate} from '../relativeDate';
b69ab3138import {
b69ab3139 latestCommitsData,
b69ab3140 latestUncommittedChangesData,
b69ab3141 mainCommandName,
b69ab3142 mergeConflicts,
b69ab3143} from '../serverAPIState';
b69ab3144import {showToast} from '../toast';
b69ab3145import {isDev} from '../utils';
b69ab3146import {ComponentExplorerButton} from './ComponentExplorer';
b69ab3147import {readInterestingAtoms, serializeAtomsState} from './getInterestingAtoms';
b69ab3148
b69ab3149import './DebugToolsMenu.css';
b69ab3150
b69ab3151/* eslint-disable no-console */
b69ab3152
b69ab3153export default function DebugToolsMenu({dismiss}: {dismiss: () => unknown}) {
b69ab3154 return (
b69ab3155 <DropdownFields
b69ab3156 title={<T>Internal Debugging Tools</T>}
b69ab3157 icon="pulse"
b69ab3158 data-testid="internal-debug-tools-dropdown"
b69ab3159 className="internal-debug-tools-dropdown">
b69ab3160 <Subtle>
b69ab3161 <T>
b69ab3162 This data is only intended for debugging Interactive Smartlog and may not capture all
b69ab3163 issues.
b69ab3164 </T>
b69ab3165 </Subtle>
b69ab3166 <DropdownField title={<T>Performance</T>}>
b69ab3167 <DebugPerfInfo />
b69ab3168 </DropdownField>
b69ab3169 <DropdownField title={<T>Commit graph</T>}>
b69ab3170 <DebugDagInfo />
b69ab3171 </DropdownField>
b69ab3172 <DropdownField title={<T>Internal State</T>}>
b69ab3173 <InternalState />
b69ab3174 </DropdownField>
b69ab3175 <DropdownField title={<T>Server/Client Messages</T>}>
b69ab3176 <Column alignStart>
b69ab3177 <ServerClientMessageLogging />
b69ab3178 <Row>
b69ab3179 <ForceDisconnectButton />
b69ab3180 <NopOperationButtons />
b69ab3181 </Row>
b69ab3182 <Row>
b69ab3183 <StressTestMessages />
b69ab3184 </Row>
b69ab3185 </Column>
b69ab3186 </DropdownField>
b69ab3187 <DropdownField title={<T>Component Explorer</T>}>
b69ab3188 <ComponentExplorerButton dismiss={dismiss} />
b69ab3189 </DropdownField>
b69ab3190 </DropdownFields>
b69ab3191 );
b69ab3192}
b69ab3193
b69ab3194function nextTick(): Promise<void> {
b69ab3195 return new Promise(res => setTimeout(res, 0));
b69ab3196}
b69ab3197
b69ab3198const stressTestAtom = atom<{progressPct: number | null; mismatches: Array<number>}>({
b69ab3199 progressPct: null,
b69ab31100 mismatches: [],
b69ab31101});
b69ab31102/** Look for out of order message passing by sending thousands of messages and verifying their ordering */
b69ab31103function StressTestMessages() {
b69ab31104 const [result, setResult] = useAtom(stressTestAtom);
b69ab31105
b69ab31106 const N = 100_000;
b69ab31107 // how many messages to send before pausing an async tick
b69ab31108 const tickEvery = 5_000;
b69ab31109
b69ab31110 const ONE_KILOBYTE = 1000;
b69ab31111 // how large of a string payload to add to each message
b69ab31112 const payloadSize = 1.0 * ONE_KILOBYTE;
b69ab31113
b69ab31114 const enableLogging = false;
b69ab31115
b69ab31116 return (
b69ab31117 <>
b69ab31118 <Button
b69ab31119 disabled={result.progressPct != null && result.progressPct !== 100}
b69ab31120 onClick={async () => {
b69ab31121 const log = enableLogging ? console.log : () => null;
b69ab31122
b69ab31123 setResult({
b69ab31124 progressPct: null,
b69ab31125 mismatches: [],
b69ab31126 });
b69ab31127 await nextTick();
b69ab31128
b69ab31129 log(' ------ Begin Stress ------');
b69ab31130 const payload = 'a'.repeat(payloadSize);
b69ab31131
b69ab31132 let lastReceivedId = 0;
b69ab31133 const dispose = serverApi.onMessageOfType('stress', ({id, time}) => {
b69ab31134 log(' < ', id, time);
b69ab31135 if (id !== lastReceivedId + 1) {
b69ab31136 setResult(last => ({...last, mismatches: [...last.mismatches, id]}));
b69ab31137 }
b69ab31138 lastReceivedId = id;
b69ab31139
b69ab31140 setResult(last => ({...last, progressPct: (100 * id) / N}));
b69ab31141 if (id == N) {
b69ab31142 dispose.dispose();
b69ab31143 setResult(last => ({...last, progressPct: 100})); // if last message was out of order, we'd get stuck
b69ab31144 log(' ------ End Stress ------');
b69ab31145 }
b69ab31146 });
b69ab31147
b69ab31148 for (let id = 1; id <= N; id++) {
b69ab31149 if (id % tickEvery === 0) {
b69ab31150 // eslint-disable-next-line no-await-in-loop
b69ab31151 await nextTick();
b69ab31152 }
b69ab31153 serverApi.postMessage({
b69ab31154 type: 'stress',
b69ab31155 id,
b69ab31156 time: new Date().valueOf(),
b69ab31157 message: payload,
b69ab31158 });
b69ab31159 log(' > ', id);
b69ab31160 }
b69ab31161 }}>
b69ab31162 <T>Message Stress Test</T>
b69ab31163 </Button>
b69ab31164 {result.progressPct == null ? null : result.progressPct < 100 ? (
b69ab31165 <Row style={{fontVariant: 'tabular-nums'}}>
b69ab31166 <Icon icon="loading" /> {Math.round(result.progressPct)}%
b69ab31167 </Row>
b69ab31168 ) : (
b69ab31169 <Tooltip title={t(`Sent ${N} messages, with ${result.mismatches.length} out of order`)}>
b69ab31170 {result.mismatches.length === 0 ? (
b69ab31171 <Icon icon="pass" color="green" />
b69ab31172 ) : (
b69ab31173 <Icon icon="error" color="red" />
b69ab31174 )}
b69ab31175 </Tooltip>
b69ab31176 )}
b69ab31177 {result.mismatches.length === 0 ? null : (
b69ab31178 <Banner kind={BannerKind.error}>{result.mismatches.join(',')}</Banner>
b69ab31179 )}
b69ab31180 </>
b69ab31181 );
b69ab31182}
b69ab31183
b69ab31184function InternalState() {
b69ab31185 const [reduxTools, setReduxTools] = useAtom(enableReduxTools);
b69ab31186 const [reactTools, setReactTools] = useAtom(enableReactTools);
b69ab31187 const needSerialize = useAtomValue(holdingCtrlAtom);
b69ab31188
b69ab31189 const generate = () => {
b69ab31190 // No need for useAtomValue - no need to re-render or recalculate this function.
b69ab31191 const atomsState = readInterestingAtoms();
b69ab31192 const value = needSerialize ? serializeAtomsState(atomsState) : atomsState;
b69ab31193 console.log(`jotai state (${needSerialize ? 'JSON' : 'objects'}):`, value);
b69ab31194 showToast(`logged jotai state to console!${needSerialize ? ' (serialized)' : ''}`);
b69ab31195 };
b69ab31196
b69ab31197 return (
b69ab31198 <Column alignStart>
b69ab31199 <Row>
b69ab31200 <Tooltip
b69ab31201 placement="bottom"
b69ab31202 title={t(
b69ab31203 'Capture a snapshot of selected Jotai atom states, log it to the dev tools console.\n\n' +
b69ab31204 'Hold Ctrl to use serialized JSON instead of Javascript objects.',
b69ab31205 )}>
b69ab31206 <Button onClick={generate}>
b69ab31207 {needSerialize ? <T>Take Snapshot (JSON)</T> : <T>Take Snapshot (objects)</T>}
b69ab31208 </Button>
b69ab31209 </Tooltip>
b69ab31210 <Tooltip
b69ab31211 placement="bottom"
b69ab31212 title={t(
b69ab31213 'Log persisted state (localStorage or vscode storage) to the dev tools console.',
b69ab31214 )}>
b69ab31215 <Button
b69ab31216 onClick={() => {
b69ab31217 console.log('persisted state:', platform.getAllPersistedState());
b69ab31218 showToast('logged persisted state to console!');
b69ab31219 }}>
b69ab31220 <T>Log Persisted State</T>
b69ab31221 </Button>
b69ab31222 </Tooltip>
b69ab31223 <Tooltip
b69ab31224 placement="bottom"
b69ab31225 title={t(
b69ab31226 'Clear any persisted state (localStorage or vscode storage). Usually only matters after restarting.',
b69ab31227 )}>
b69ab31228 <Button
b69ab31229 onClick={() => {
b69ab31230 platform.clearPersistedState();
b69ab31231 console.log('--- cleared isl persisted state ---');
b69ab31232 showToast('cleared persisted state');
b69ab31233 }}>
b69ab31234 <T>Clear Persisted State</T>
b69ab31235 </Button>
b69ab31236 </Tooltip>
b69ab31237 </Row>
b69ab31238 {isDev && (
b69ab31239 <Row>
b69ab31240 <T>Integrate with: </T>
b69ab31241 <Checkbox checked={reduxTools} onChange={setReduxTools}>
b69ab31242 <T>Redux DevTools</T>
b69ab31243 </Checkbox>
b69ab31244 <Checkbox checked={reactTools} onChange={setReactTools}>
b69ab31245 {t('React <DebugAtoms/>')}
b69ab31246 </Checkbox>
b69ab31247 </Row>
b69ab31248 )}
b69ab31249 </Column>
b69ab31250 );
b69ab31251}
b69ab31252
b69ab31253const logMessagesState = atomWithOnChange(
b69ab31254 atom(debugLogMessageTraffic.shouldLog),
b69ab31255 newValue => {
b69ab31256 debugLogMessageTraffic.shouldLog = newValue;
b69ab31257 console.log(`----- ${newValue ? 'Enabled' : 'Disabled'} Logging Messages -----`);
b69ab31258 },
b69ab31259 /* skipInitialCall */ true,
b69ab31260);
b69ab31261
b69ab31262function ServerClientMessageLogging() {
b69ab31263 const [shouldLog, setShouldLog] = useAtom(logMessagesState);
b69ab31264 return (
b69ab31265 <div>
b69ab31266 <Checkbox checked={shouldLog} onChange={checked => setShouldLog(checked)}>
b69ab31267 <T>Log messages</T>
b69ab31268 </Checkbox>
b69ab31269 </div>
b69ab31270 );
b69ab31271}
b69ab31272
b69ab31273function DebugPerfInfo() {
b69ab31274 const latestStatus = useAtomValue(latestUncommittedChangesData);
b69ab31275 const latestLog = useAtomValue(latestCommitsData);
b69ab31276 const latestConflicts = useAtomValue(mergeConflicts);
b69ab31277 const heartbeat = useHeartbeat();
b69ab31278 const commandName = useAtomValue(mainCommandName);
b69ab31279 return (
b69ab31280 <div>
b69ab31281 {heartbeat.type === 'timeout' ? (
b69ab31282 <InlineErrorBadge error={new Error(t('Heartbeat timeout'))}>
b69ab31283 <T>Heartbeat timed out</T>
b69ab31284 </InlineErrorBadge>
b69ab31285 ) : (
b69ab31286 <FetchDurationInfo
b69ab31287 name={<T>ISL Server Ping</T>}
b69ab31288 duration={(heartbeat as Heartbeat & {type: 'success'})?.rtt}
b69ab31289 />
b69ab31290 )}
b69ab31291 <FetchDurationInfo
b69ab31292 name={<T replace={{$cmd: commandName}}>$cmd status</T>}
b69ab31293 start={latestStatus.fetchStartTimestamp}
b69ab31294 end={latestStatus.fetchCompletedTimestamp}
b69ab31295 />
b69ab31296 <FetchDurationInfo
b69ab31297 name={<T replace={{$cmd: commandName}}>$cmd log</T>}
b69ab31298 start={latestLog.fetchStartTimestamp}
b69ab31299 end={latestLog.fetchCompletedTimestamp}
b69ab31300 />
b69ab31301 <FetchDurationInfo
b69ab31302 name={<T>Merge Conflicts</T>}
b69ab31303 start={latestConflicts?.fetchStartTimestamp}
b69ab31304 end={latestConflicts?.fetchCompletedTimestamp}
b69ab31305 />
b69ab31306 </div>
b69ab31307 );
b69ab31308}
b69ab31309
b69ab31310const styles = stylex.create({
b69ab31311 slow: {
b69ab31312 color: colors.signalFg,
b69ab31313 backgroundColor: colors.signalBadBg,
b69ab31314 },
b69ab31315 ok: {
b69ab31316 color: colors.signalFg,
b69ab31317 backgroundColor: colors.signalMediumBg,
b69ab31318 },
b69ab31319 fast: {
b69ab31320 color: colors.signalFg,
b69ab31321 backgroundColor: colors.signalGoodBg,
b69ab31322 },
b69ab31323});
b69ab31324
b69ab31325function FetchDurationInfo(
b69ab31326 props: {name: ReactNode} & ExclusiveOr<{start?: number; end?: number}, {duration: number}>,
b69ab31327) {
b69ab31328 const {name} = props;
b69ab31329 const {end, start, duration} = props;
b69ab31330 const deltaMs = duration != null ? duration : end == null || start == null ? null : end - start;
b69ab31331 const xstyle =
b69ab31332 deltaMs == null
b69ab31333 ? undefined
b69ab31334 : deltaMs < 1000
b69ab31335 ? styles.fast
b69ab31336 : deltaMs < 3000
b69ab31337 ? styles.ok
b69ab31338 : styles.slow;
b69ab31339 return (
b69ab31340 <div className={`fetch-duration-info`}>
b69ab31341 {name} <Badge xstyle={xstyle}>{deltaMs == null ? 'N/A' : `${deltaMs}ms`}</Badge>
b69ab31342 {end == null ? null : (
b69ab31343 <Subtle>
b69ab31344 <Tooltip title={new Date(end).toLocaleString()} placement="right">
b69ab31345 <RelativeDate date={end} />
b69ab31346 </Tooltip>
b69ab31347 </Subtle>
b69ab31348 )}
b69ab31349 </div>
b69ab31350 );
b69ab31351}
b69ab31352
b69ab31353function useMeasureDuration(slowOperation: () => void): number | null {
b69ab31354 const [measured, setMeasured] = useState<null | number>(null);
b69ab31355 useEffect(() => {
b69ab31356 requestIdleCallback(() => {
b69ab31357 const startTime = performance.now();
b69ab31358 slowOperation();
b69ab31359 const endTime = performance.now();
b69ab31360 setMeasured(endTime - startTime);
b69ab31361 });
b69ab31362 return () => setMeasured(null);
b69ab31363 }, [slowOperation]);
b69ab31364 return measured;
b69ab31365}
b69ab31366
b69ab31367function DebugDagInfo() {
b69ab31368 const dag = useAtomValue(dagWithPreviews);
b69ab31369 const dagRenderBenchmark = useCallback(() => {
b69ab31370 // Slightly change the dag to invalidate its caches.
b69ab31371 const noise = performance.now();
b69ab31372 const newDag = dag.add([DagCommitInfo.fromCommitInfo({hash: `dummy-${noise}`, parents: []})]);
b69ab31373 newDag.renderToRows(newDag.subsetForRendering());
b69ab31374 }, [dag]);
b69ab31375
b69ab31376 const dagSize = dag.all().size;
b69ab31377 const dagDisplayedSize = dag.subsetForRendering().size;
b69ab31378 const dagSortMs = useMeasureDuration(dagRenderBenchmark);
b69ab31379
b69ab31380 return (
b69ab31381 <div>
b69ab31382 <T>Size: </T>
b69ab31383 {dagSize}
b69ab31384 <br />
b69ab31385 <T>Displayed: </T>
b69ab31386 {dagDisplayedSize}
b69ab31387 <br />
b69ab31388 <>
b69ab31389 <T>Render calculation: </T>
b69ab31390 {dagSortMs == null ? (
b69ab31391 <T>(Measuring)</T>
b69ab31392 ) : (
b69ab31393 <>
b69ab31394 {dagSortMs.toFixed(1)} <T>ms</T>
b69ab31395 </>
b69ab31396 )}
b69ab31397 <br />
b69ab31398 </>
b69ab31399 </div>
b69ab31400 );
b69ab31401}
b69ab31402
b69ab31403const forceDisconnectDuration = atom<number>(3000);
b69ab31404
b69ab31405function ForceDisconnectButton() {
b69ab31406 const [duration, setDuration] = useAtom(forceDisconnectDuration);
b69ab31407 const forceDisconnect = platform.messageBus.forceDisconnect?.bind(platform.messageBus);
b69ab31408 if (forceDisconnect == null) {
b69ab31409 return null;
b69ab31410 }
b69ab31411 return (
b69ab31412 <Button
b69ab31413 onClick={() => forceDisconnect(duration)}
b69ab31414 onWheel={e => {
b69ab31415 // deltaY is usually -100 +100 per event.
b69ab31416 const dy = e.deltaY;
b69ab31417 const scale = duration < 20000 ? 10 : 100;
b69ab31418 if (dy > 0) {
b69ab31419 setDuration(v => Math.max(v - dy * scale, 1000));
b69ab31420 } else if (dy < 0) {
b69ab31421 setDuration(v => Math.min(v - dy * scale, 300000));
b69ab31422 }
b69ab31423 }}>
b69ab31424 <T replace={{$sec: (duration / 1000).toFixed(1)}}>Force disconnect for $sec seconds</T>
b69ab31425 </Button>
b69ab31426 );
b69ab31427}
b69ab31428
b69ab31429function NopOperationButtons() {
b69ab31430 const runOperation = useRunOperation();
b69ab31431 return (
b69ab31432 <>
b69ab31433 {[2, 5, 20].map(durationSeconds => (
b69ab31434 <Button
b69ab31435 key={durationSeconds}
b69ab31436 onClick={() => runOperation(new NopOperation(durationSeconds))}>
b69ab31437 <T replace={{$sec: durationSeconds}}>Nop $sec s</T>
b69ab31438 </Button>
b69ab31439 ))}
b69ab31440 </>
b69ab31441 );
b69ab31442}