diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index 60ed03a466a9..7a04b2f30b75 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -12,6 +12,7 @@ import { import { setGlobalOffsetTop } from "../../../states/core"; import { getSnapshot } from "../../../states/snapshot"; import { cn } from "../../../utils/cn"; +import { isProfilerMode } from "../../../utils/profiler-mode"; import { Fa } from "../../common/Fa"; import { showUpdateNameModal } from "../../modals/account-settings/UpdateNameModal"; @@ -111,6 +112,14 @@ export function Banners(): JSXElement { onMount(() => { window.addEventListener("resize", debouncedMarginUpdate); + if (isProfilerMode()) { + addBanner({ + level: "error", + icon: "fas fa-stopwatch", + text: "Profiler mode enabled", + important: true, + }); + } }); onCleanup(() => { diff --git a/frontend/src/ts/components/modals/DevOptionsModal.tsx b/frontend/src/ts/components/modals/DevOptionsModal.tsx index 4f78c89a6367..0a4c767a87e3 100644 --- a/frontend/src/ts/components/modals/DevOptionsModal.tsx +++ b/frontend/src/ts/components/modals/DevOptionsModal.tsx @@ -21,6 +21,7 @@ import { toggleUserFakeChartData } from "../../test/result"; import { disableSlowTimerFail } from "../../test/test-timer"; import { FaSolidIcon } from "../../types/font-awesome"; import { setMediaQueryDebugLevel } from "../../ui"; +import { isProfilerMode, setProfilerMode } from "../../utils/profiler-mode"; import { remoteValidation } from "../../utils/remote-validation"; import { AnimatedModal } from "../common/AnimatedModal"; import { Button } from "../common/Button"; @@ -171,6 +172,15 @@ export function DevOptionsModal(): JSXElement { label: () => "Event Log Viewer", onClick: () => showModal("EventLogViewer"), }, + { + icon: "fa-stopwatch", + label: () => `Profiler Mode (${isProfilerMode() ? "ON" : "OFF"})`, + onClick: () => { + setProfilerMode(!isProfilerMode()); + showNoticeNotification("Profiler mode toggled, reloading..."); + setTimeout(() => location.reload(), 500); + }, + }, ]; const addDebugInboxItem = (rewardType: "xp" | "badge" | "none"): void => { diff --git a/frontend/src/ts/cookies.ts b/frontend/src/ts/cookies.ts index b5a0bc34ef36..ad877ce9a98a 100644 --- a/frontend/src/ts/cookies.ts +++ b/frontend/src/ts/cookies.ts @@ -3,6 +3,7 @@ import { createSignal } from "solid-js"; import { LocalStorageWithSchema } from "./utils/local-storage-with-schema"; import { activateAnalytics } from "./controllers/analytics-controller"; import { activateSentry } from "./sentry"; +import { isProfilerMode } from "./utils/profiler-mode"; const AcceptedCookiesSchema = z .object({ @@ -39,7 +40,7 @@ export function activateWhatsAccepted(): void { if (accepted?.analytics) { activateAnalytics(); } - if (accepted?.sentry) { + if (accepted?.sentry && !isProfilerMode()) { void activateSentry(); } } diff --git a/frontend/src/ts/dev/signal-tracker.ts b/frontend/src/ts/dev/signal-tracker.ts index 4f1b6994a2a5..815654200714 100644 --- a/frontend/src/ts/dev/signal-tracker.ts +++ b/frontend/src/ts/dev/signal-tracker.ts @@ -1,4 +1,5 @@ import { createSignal, DEV } from "solid-js"; +import { isProfilerMode } from "../utils/profiler-mode"; export type TrackedSignal = { name: string; @@ -91,7 +92,7 @@ function formatInitialValue(value: unknown): string { } } -if (DEV) { +if (DEV && !isProfilerMode()) { type NodeInfo = { name: string; type: "signal" | "store"; diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index 78d0fb852930..a7594f6e220c 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -9,6 +9,7 @@ import { KeydownEventData, KeyupEvent, KeyupEventData, + TestEvent, TestEventData, TestEventNoMs, TestEventType, @@ -27,19 +28,25 @@ import { isFunboxActiveWithProperty } from "../funbox/list"; import { getCurrentQuote } from "../../states/test"; export function buildEventLog(): EventLog { - return { - version: EVENT_LOG_VERSION, - events: getAllTestEvents(), - context: { - targetWords: [...TestWords.words.list], - mode: Config.mode, - mode2: getMode2(Config, getCurrentQuote()), + const context = { + targetWords: [...TestWords.words.list], + mode: Config.mode, + mode2: getMode2(Config, getCurrentQuote()), + koreanStatus: koreanStatus, + bailedOut: bailedOut, + ...(Config.mode === "custom" && { customTextLimitMode: CustomText.getLimit().mode, customTextLimitValue: CustomText.getLimit().value, + }), + ...(Config.funbox.length !== 0 && { isFunboxWithNospacePropertyActive: isFunboxActiveWithProperty("nospace"), - koreanStatus: koreanStatus, - bailedOut: bailedOut, - }, + }), + }; + + return { + version: EVENT_LOG_VERSION, + events: getAllTestEvents(), + context, }; } @@ -272,20 +279,31 @@ export function getAllTestEvents(): TestEventNoMs[] { const startEventMs = timerEvents.find((e) => e.data.event === "start")?.ms ?? firstEventMs ?? 0; - // cachedAllEvents = testData300; - // return cachedAllEvents; - cachedAllEvents = [ - ...keydownEvents, - ...keyupEvents, - ...timerEvents, - ...inputEvents, - ...compositionEvents, - ] + const total = + keydownEvents.length + + keyupEvents.length + + timerEvents.length + + inputEvents.length + + compositionEvents.length; + + const merged = new Array(total); + let p = 0; + for (const e of keydownEvents) merged[p++] = e; + for (const e of keyupEvents) merged[p++] = e; + for (const e of timerEvents) merged[p++] = e; + for (const e of inputEvents) merged[p++] = e; + for (const e of compositionEvents) merged[p++] = e; + + cachedAllEvents = merged .sort((a, b) => a.ms - b.ms || sortTieRank(a.type) - sortTieRank(b.type)) - .map(({ ms, ...rest }) => ({ - ...rest, - testMs: roundTo2(ms - startEventMs), - })); + .map( + (event) => + ({ + type: event.type, + testMs: roundTo2(event.ms - startEventMs), + data: event.data, + }) as TestEventNoMs, + ); return cachedAllEvents; } diff --git a/frontend/src/ts/test/events/types.ts b/frontend/src/ts/test/events/types.ts index 5095c3ea8353..60566bdbd56a 100644 --- a/frontend/src/ts/test/events/types.ts +++ b/frontend/src/ts/test/events/types.ts @@ -129,10 +129,10 @@ export type EventLogContext = { // isTimedTest: boolean; mode: Config["mode"]; mode2: ReturnType; - customTextLimitMode: CustomTextLimitMode; - customTextLimitValue: number; + customTextLimitMode?: CustomTextLimitMode; + customTextLimitValue?: number; + isFunboxWithNospacePropertyActive?: boolean; bailedOut: boolean; - isFunboxWithNospacePropertyActive: boolean; koreanStatus: boolean; }; diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 0e6c155d6b1b..1bc5b2ed6825 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -1277,16 +1277,22 @@ function buildWordLettersHTML( ) { extraCorrected = "extraCorrected"; } + + let displayLetter = inputCharacters[c]; + if (displayLetter === " ") { + displayLetter = "_"; + } + if (Config.mode === "zen" || wordCharacters[c] !== undefined) { if (Config.mode === "zen" || inputCharacters[c] === wordCharacters[c]) { if ( correctedChar === inputCharacters[c] || correctedChar === undefined ) { - out += `${inputCharacters[c]}`; + out += `${displayLetter}`; } else { out += `${ - inputCharacters[c] + displayLetter }`; } } else { @@ -1303,7 +1309,7 @@ function buildWordLettersHTML( } } } else { - out += `${inputCharacters[c]}`; + out += `${displayLetter}`; } } return out; @@ -1391,7 +1397,10 @@ async function loadWordsHistory(): Promise { ); } catch (e) { try { - for (const char of word) { + for (let char of word) { + if (char === " ") { + char = "_"; + } const letterEl = document.createElement("letter"); letterEl.textContent = char; wordEl.appendChild(letterEl); diff --git a/frontend/src/ts/utils/logger.ts b/frontend/src/ts/utils/logger.ts index 1415471d3fe8..db81060e50a3 100644 --- a/frontend/src/ts/utils/logger.ts +++ b/frontend/src/ts/utils/logger.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { LocalStorageWithSchema } from "./local-storage-with-schema"; import { isDevEnvironment } from "./env"; +import { isProfilerMode } from "./profiler-mode"; const nativeLog = console.log; const nativeWarn = console.warn; @@ -14,7 +15,9 @@ const debugLogsLS = new LocalStorageWithSchema({ let debugLogs = debugLogsLS.get(); -if (isDevEnvironment()) { +if (isProfilerMode()) { + debugLogs = false; +} else if (isDevEnvironment()) { debugLogs = true; debug("Debug logs automatically enabled on localhost"); } diff --git a/frontend/src/ts/utils/profiler-mode.ts b/frontend/src/ts/utils/profiler-mode.ts new file mode 100644 index 000000000000..ca4aeeda667f --- /dev/null +++ b/frontend/src/ts/utils/profiler-mode.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; +import { LocalStorageWithSchema } from "./local-storage-with-schema"; +import { isDevEnvironment } from "./env"; + +const profilerModeLS = new LocalStorageWithSchema({ + key: "profilerMode", + schema: z.boolean(), + fallback: false, +}); + +// Resolved at module load: profiler mode disables features that initialise +// at load (signal tracker hook, sentry init, logger debug filter), so toggling +// it requires a reload to take effect. +const active = isDevEnvironment() && profilerModeLS.get(); + +export function isProfilerMode(): boolean { + return active; +} + +export function setProfilerMode(value: boolean): void { + if (!isDevEnvironment()) return; + profilerModeLS.set(value); +}