Repository: preactjs/preact-devtools (ts)

Issue:
Add reload and profile
There should be a button in the Profiler that reloads the page and immediately starts the recording timing data. Without that it's not possible to profile the first few commits and show them in the devtools.

Pull request description:
Add support for reload and profile

![preact-reload-profile](https://user-images.githubusercontent.com/1062408/80277798-d65ed180-86f1-11ea-956e-95654fdcdc15.png)

Fixes #152 .

Patch:
diff --git a/src/adapter/adapter/adapter.ts b/src/adapter/adapter/adapter.ts
index 3025327..1e4ca45 100644
--- a/src/adapter/adapter/adapter.ts
+++ b/src/adapter/adapter/adapter.ts
@@ -7,6 +7,7 @@ import { createHightlighter } from "./highlight";
 import { parseFilters } from "./filter";
 import { PortPageHook } from "./port";
 import { PropData } from "../../view/components/sidebar/inspect/parseProps";
+import { PROFILE_RELOAD } from "../../constants";
 
 export type Path = Array<string | number>;
 
@@ -125,4 +126,14 @@ export function createAdapter(port: PortPageHook, renderer: Renderer) {
 	// Profiler
 	listen("start-profiling", options => renderer.startProfiling!(options));
 	listen("stop-profiling", () => renderer.stopProfiling!());
+	listen("reload-and-profile", options => {
+		window.localStorage.setItem(PROFILE_RELOAD, JSON.stringify(options));
+
+		try {
+			window.location.reload();
+		} catch (err) {
+			console.error("Preact Devtools was not able to reload the current page.");
+			console.log(err);
+		}
+	});
 }
diff --git a/src/adapter/hook.ts b/src/adapter/hook.ts
index 6532077..4b83d51 100644
--- a/src/adapter/hook.ts
+++ b/src/adapter/hook.ts
@@ -8,9 +8,14 @@ import { setupOptions } from "./10/options";
 import { createMultiRenderer } from "./MultiRenderer";
 import parseSemverish from "./parse-semverish";
 import { PortPageHook } from "./adapter/port";
+import { PROFILE_RELOAD } from "../constants";
 
 export type EmitterFn = (event: string, data: any) => void;
 
+export interface ProfilerOptions {
+	captureRenderReasons?: boolean;
+}
+
 export interface DevtoolEvents {
 	"update-prop": { id: ID; path: string; value: any };
 	"update-state": { id: ID; path: string; value: any };
@@ -19,9 +24,10 @@ export interface DevtoolEvents {
 	"force-update": ID;
 	"start-picker": null;
 	"stop-picker": null;
-	"start-profiling": { captureRenderReasons?: boolean };
+	"start-profiling": ProfilerOptions;
 	"stop-profiling": null;
 	"clear-profiling": null;
+	"reload-and-profile": ProfilerOptions;
 	"update-filter": RawFilterState;
 	copy: string;
 	highlight: ID | null;
@@ -100,6 +106,15 @@ export function createHook(port: PortPageHook): DevtoolsHook {
 			supportsRenderReasons: !!supports.renderReasons,
 			supportsHooks: !!supports.hooks,
 		});
+
+		// Feature: Profile and reaload
+		// Check if we should immediately start profiling on create
+		const profilerOptions = window.localStorage.getItem(PROFILE_RELOAD);
+		if (profilerOptions !== null) {
+			window.localStorage.removeItem(PROFILE_RELOAD);
+			renderer.startProfiling!(JSON.parse(profilerOptions));
+		}
+
 		return uid;
 	};
 
diff --git a/src/constants.ts b/src/constants.ts
index 20c00d7..67e7e19 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -11,6 +11,8 @@ export enum Status {
 	Pending = "pending",
 }
 
+export const PROFILE_RELOAD = "preact-devtools_profile-and-reload";
+
 export enum HookType {
 	useState = 1,
 	useReducer = 2,
diff --git a/src/view/components/profiler/components/TimelineBar/TimelineBar.tsx b/src/view/components/profiler/components/TimelineBar/TimelineBar.tsx
index 73d61eb..ef14ef8 100644
--- a/src/view/components/profiler/components/TimelineBar/TimelineBar.tsx
+++ b/src/view/components/profiler/components/TimelineBar/TimelineBar.tsx
@@ -3,7 +3,7 @@ import { Actions, ActionSeparator } from "../../../Actions";
 import { CommitTimeline } from "../CommitTimeline/CommitTimeline";
 import { IconBtn } from "../../../IconBtn";
 import { useStore, useObserver } from "../../../../store/react-bindings";
-import { RecordIcon, NotInterested } from "../../../icons";
+import { RecordIcon, NotInterested, Refresh } from "../../../icons";
 import s from "../../../elements/TreeBar.css";
 import { useCallback } from "preact/hooks";
 import { FlameGraphMode } from "../../flamegraph/FlameGraphMode";
@@ -26,6 +26,13 @@ export function TimelineBar() {
 		[store],
 	);
 
+	const onReloadAndProfile = useCallback(() => {
+		store.profiler.isRecording.$ = true;
+		store.emit("reload-and-profile", {
+			captureRenderReasons: store.profiler.captureRenderReasons.$,
+		});
+	}, []);
+
 	const onReset = useCallback(() => {
 		resetProfiler(store.profiler);
 	}, [store]);
@@ -35,6 +42,16 @@ export function TimelineBar() {
 			<div class={s.btnWrapper}>
 				<RecordBtn />
 			</div>
+			<div class={s.btnWrapper}>
+				<IconBtn
+					title="Reload and profile"
+					disabled={!isSupported || isRecording}
+					testId="reload-and-profile-btn"
+					onClick={onReloadAndProfile}
+				>
+					<Refresh size="s" />
+				</IconBtn>
+			</div>
 			<div class={s.btnWrapper}>
 				<IconBtn
 					title="Clear profiling data"
@@ -72,7 +89,17 @@ export function RecordBtn() {
 	const isSupported = useObserver(() => store.profiler.isSupported.$);
 
 	const onClick = useCallback(() => {
-		store.profiler.isRecording.$ = !store.profiler.isRecording.$;
+		const { isRecording, captureRenderReasons } = store.profiler;
+		const v = !isRecording.$;
+		isRecording.$ = v;
+
+		if (v) {
+			store.emit("start-profiling", {
+				captureRenderReasons: captureRenderReasons.$,
+			});
+		} else {
+			store.emit("stop-profiling", null);
+		}
 	}, [store]);
 
 	return (
diff --git a/src/view/components/profiler/data/commits.ts b/src/view/components/profiler/data/commits.ts
index 73f01fe..39138c6 100644
--- a/src/view/components/profiler/data/commits.ts
+++ b/src/view/components/profiler/data/commits.ts
@@ -6,7 +6,6 @@ import {
 	RenderReasonMap,
 	RenderReasonData,
 } from "../../../../adapter/10/renderer/renderReasons";
-import { EmitFn } from "../../../../adapter/hook";
 
 export interface ProfilerNode extends DevNode {
 	selfDuration: number;
@@ -70,7 +69,7 @@ export interface ProfilerState {
  * Create a new profiler instance. It intentiall doesn't have
  * any methods, to not go down the OOP rabbit hole.
  */
-export function createProfiler(emit: EmitFn): ProfilerState {
+export function createProfiler(): ProfilerState {
 	const commits = valoo<CommitData[]>([]);
 	const isSupported = valoo(false);
 
@@ -110,12 +109,6 @@ export function createProfiler(emit: EmitFn): ProfilerState {
 				selectedNodeId.$ = commits.$[0].rootId;
 			}
 		}
-
-		if (v) {
-			emit("start-profiling", { captureRenderReasons: captureRenderReasons.$ });
-		} else {
-			emit("stop-profiling", null);
-		}
 	});
 
 	// Flamegraph
diff --git a/src/view/store/index.ts b/src/view/store/index.ts
index 7adab16..f9e917c 100644
--- a/src/view/store/index.ts
+++ b/src/view/store/index.ts
@@ -108,7 +108,7 @@ export function createStore(): Store {
 		supports: {
 			hooks: supportsHooks,
 		},
-		profiler: createProfiler(notify),
+		profiler: createProfiler(),
 		notify,
 		nodeList,
 		inspectData,


Test patch:
diff --git a/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts b/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts
index e3dcadb..0ffceba 100644
--- a/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts
+++ b/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts
@@ -8,7 +8,7 @@ describe("FlameGraphStore", () => {
 	let flame: ReturnType<typeof createFlameGraphStore>;
 
 	beforeEach(() => {
-		profiler = createProfiler(() => null);
+		profiler = createProfiler();
 		flame = createFlameGraphStore(profiler);
 		profiler.activeCommitIdx.$ = 0;
 	});
diff --git a/test-e2e/test-utils.ts b/test-e2e/test-utils.ts
index afd8d86..7a1948f 100644
--- a/test-e2e/test-utils.ts
+++ b/test-e2e/test-utils.ts
@@ -12,7 +12,7 @@ const readFile = (name: string) => {
 	);
 };
 
-const TEST_URL = "http://test-preact-devtools.dev";
+const TEST_URL = "http://localhost";
 
 export function mockResponse(req: Request, test: string, file: string) {
 	const mime = file.endsWith(".html")
diff --git a/test-e2e/tests/fixtures/profile-reload.js b/test-e2e/tests/fixtures/profile-reload.js
new file mode 100644
index 0000000..a973626
--- /dev/null
+++ b/test-e2e/tests/fixtures/profile-reload.js
@@ -0,0 +1,9 @@
+{
+	const app = document.getElementById("app");
+
+	const iframe = document.createElement("iframe");
+	iframe.id = "test-case";
+	iframe.src = "/iframe.html";
+
+	app.appendChild(iframe);
+}
diff --git a/test-e2e/tests/profiler-reload.test.ts b/test-e2e/tests/profiler-reload.test.ts
new file mode 100644
index 0000000..10e6979
--- /dev/null
+++ b/test-e2e/tests/profiler-reload.test.ts
@@ -0,0 +1,34 @@
+import { newTestPage, click } from "../test-utils";
+import { closePage } from "pentf/browser_utils";
+
+export const description = "Reload and immediately start profiling";
+// Reloading the whole page breaks our current test setup, and
+// I don't know of a way to patch `window.location.reload` on the fly.
+export const skip = () => true;
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "profile-reload", {
+		preact: "next",
+	});
+
+	const iframe = 'iframe[src="/iframe.html"]';
+
+	await page.waitForSelector(iframe);
+
+	await click(devtools, '[name="root-panel"][value="PROFILER"]');
+
+	const btn = '[data-testid="reload-and-profile-btn"]';
+	await click(devtools, btn);
+
+	await page.waitForSelector(iframe);
+
+	await page.$eval(iframe, el => {
+		const btn = (el as HTMLIFrameElement).contentWindow!.document.querySelector(
+			"button",
+		)!;
+		btn.click();
+		btn.click();
+	});
+
+	await closePage(page);
+}