@clary-so/measure 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.postcssrc +5 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/dist/__tests__/measure.test.d.ts +1 -0
- package/dist/__tests__/measure.test.js +152 -0
- package/dist/__tests__/measure.test.js.map +1 -0
- package/dist/__tests__/server/ServerApp.test.d.ts +1 -0
- package/dist/__tests__/server/ServerApp.test.js +49 -0
- package/dist/__tests__/server/ServerApp.test.js.map +1 -0
- package/dist/__tests__/utils/removeCLIColors.d.ts +1 -0
- package/dist/__tests__/utils/removeCLIColors.js +10 -0
- package/dist/__tests__/utils/removeCLIColors.js.map +1 -0
- package/dist/__tests__/webapp/socket.test.d.ts +1 -0
- package/dist/__tests__/webapp/socket.test.js +73 -0
- package/dist/__tests__/webapp/socket.test.js.map +1 -0
- package/dist/common/useLogSocketEvents.d.ts +3 -0
- package/dist/common/useLogSocketEvents.js +18 -0
- package/dist/common/useLogSocketEvents.js.map +1 -0
- package/dist/index.87c99d25.js +88 -0
- package/dist/index.87c99d25.js.map +1 -0
- package/dist/index.html +1 -0
- package/dist/server/ServerApp.d.ts +10 -0
- package/dist/server/ServerApp.js +131 -0
- package/dist/server/ServerApp.js.map +1 -0
- package/dist/server/ServerSocketConnectionApp.d.ts +6 -0
- package/dist/server/ServerSocketConnectionApp.js +105 -0
- package/dist/server/ServerSocketConnectionApp.js.map +1 -0
- package/dist/server/bin.d.ts +2 -0
- package/dist/server/bin.js +63 -0
- package/dist/server/bin.js.map +1 -0
- package/dist/server/components/HostAndPortInfo.d.ts +4 -0
- package/dist/server/components/HostAndPortInfo.js +14 -0
- package/dist/server/components/HostAndPortInfo.js.map +1 -0
- package/dist/server/constants.d.ts +2 -0
- package/dist/server/constants.js +12 -0
- package/dist/server/constants.js.map +1 -0
- package/dist/server/socket/socketInterface.d.ts +37 -0
- package/dist/server/socket/socketInterface.js +17 -0
- package/dist/server/socket/socketInterface.js.map +1 -0
- package/dist/server/socket/socketState.d.ts +5 -0
- package/dist/server/socket/socketState.js +47 -0
- package/dist/server/socket/socketState.js.map +1 -0
- package/dist/server/useBundleIdControls.d.ts +2 -0
- package/dist/server/useBundleIdControls.js +33 -0
- package/dist/server/useBundleIdControls.js.map +1 -0
- package/dist/webapp/MeasureWebApp.d.ts +2 -0
- package/dist/webapp/MeasureWebApp.js +29 -0
- package/dist/webapp/MeasureWebApp.js.map +1 -0
- package/dist/webapp/components/AppBar.d.ts +4 -0
- package/dist/webapp/components/AppBar.js +19 -0
- package/dist/webapp/components/AppBar.js.map +1 -0
- package/dist/webapp/components/BundleIdSelector.d.ts +6 -0
- package/dist/webapp/components/BundleIdSelector.js +20 -0
- package/dist/webapp/components/BundleIdSelector.js.map +1 -0
- package/dist/webapp/components/SocketState.d.ts +2 -0
- package/dist/webapp/components/SocketState.js +95 -0
- package/dist/webapp/components/SocketState.js.map +1 -0
- package/dist/webapp/components/StartButton.d.ts +6 -0
- package/dist/webapp/components/StartButton.js +12 -0
- package/dist/webapp/components/StartButton.js.map +1 -0
- package/dist/webapp/components/TextField.d.ts +5 -0
- package/dist/webapp/components/TextField.js +81 -0
- package/dist/webapp/components/TextField.js.map +1 -0
- package/dist/webapp/socket.d.ts +3 -0
- package/dist/webapp/socket.js +8 -0
- package/dist/webapp/socket.js.map +1 -0
- package/dist/webapp/useMeasures.d.ts +10 -0
- package/dist/webapp/useMeasures.js +38 -0
- package/dist/webapp/useMeasures.js.map +1 -0
- package/package.json +48 -0
- package/src/__tests__/__snapshots__/measure.test.tsx.snap +4389 -0
- package/src/__tests__/measure.test.tsx +141 -0
- package/src/__tests__/server/ServerApp.test.ts +49 -0
- package/src/__tests__/utils/removeCLIColors.ts +5 -0
- package/src/__tests__/webapp/socket.test.ts +37 -0
- package/src/common/types/index.d.ts +3 -0
- package/src/common/useLogSocketEvents.ts +17 -0
- package/src/server/ServerApp.tsx +103 -0
- package/src/server/ServerSocketConnectionApp.tsx +82 -0
- package/src/server/bin.tsx +23 -0
- package/src/server/components/HostAndPortInfo.tsx +11 -0
- package/src/server/constants.ts +8 -0
- package/src/server/socket/socketInterface.ts +53 -0
- package/src/server/socket/socketState.ts +66 -0
- package/src/server/useBundleIdControls.ts +38 -0
- package/src/webapp/MeasureWebApp.tsx +43 -0
- package/src/webapp/components/AppBar.tsx +19 -0
- package/src/webapp/components/BundleIdSelector.tsx +26 -0
- package/src/webapp/components/SocketState.tsx +79 -0
- package/src/webapp/components/StartButton.tsx +22 -0
- package/src/webapp/components/TextField.tsx +54 -0
- package/src/webapp/globals.d.ts +9 -0
- package/src/webapp/index.html +30 -0
- package/src/webapp/index.js +9 -0
- package/src/webapp/socket.ts +12 -0
- package/src/webapp/useMeasures.ts +36 -0
- package/tailwind.config.js +7 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import "@clary-so/e2e/src/utils/test/mockChildProcess";
|
|
2
|
+
import {
|
|
3
|
+
emitMeasures,
|
|
4
|
+
perfProfilerMock,
|
|
5
|
+
aTraceMock,
|
|
6
|
+
} from "@clary-so/e2e/src/utils/test/mockEmitMeasures";
|
|
7
|
+
import { fireEvent, render as webRender, screen, waitFor, act } from "@testing-library/react";
|
|
8
|
+
import { render as cliRender } from "ink-testing-library";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { ServerApp } from "../server/ServerApp";
|
|
11
|
+
import { open } from "@clary-so/shell";
|
|
12
|
+
import { matchSnapshot } from "@clary-so/web-reporter-ui/utils/testUtils";
|
|
13
|
+
import { removeCLIColors } from "./utils/removeCLIColors";
|
|
14
|
+
import { LogLevel, Logger } from "@clary-so/logger";
|
|
15
|
+
import { DEFAULT_PORT } from "../server/constants";
|
|
16
|
+
|
|
17
|
+
jest.mock("@clary-so/shell", () => ({
|
|
18
|
+
open: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
Math.random = () => 0.5;
|
|
22
|
+
|
|
23
|
+
// Set me to LogLevel.DEBUG to see the debug logs
|
|
24
|
+
Logger.setLogLevel(LogLevel.SILENT);
|
|
25
|
+
|
|
26
|
+
let originalWindow: Window & typeof globalThis;
|
|
27
|
+
let MeasureWebApp: React.FC;
|
|
28
|
+
|
|
29
|
+
describe("flashlight measure interactive", () => {
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
originalWindow = global.window;
|
|
32
|
+
|
|
33
|
+
global.window = Object.create(window);
|
|
34
|
+
Object.defineProperty(window, "__FLASHLIGHT_DATA__", {
|
|
35
|
+
value: { socketServerUrl: `http://localhost:${DEFAULT_PORT}` },
|
|
36
|
+
writable: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
MeasureWebApp = (await import("../webapp/MeasureWebApp")).MeasureWebApp;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
global.window = originalWindow;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const expectWebAppToBeOpened = () =>
|
|
47
|
+
waitFor(() => expect(open).toHaveBeenCalledWith(`http://localhost:${DEFAULT_PORT}`));
|
|
48
|
+
|
|
49
|
+
const setupCli = (customPort = DEFAULT_PORT) => {
|
|
50
|
+
const { lastFrame, unmount } = cliRender(<ServerApp port={customPort} />);
|
|
51
|
+
const closeCli = async () => {
|
|
52
|
+
unmount();
|
|
53
|
+
// Seems like we need to wait for the useEffect cleanup to happen
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
closeCli,
|
|
59
|
+
expectCliOutput: () => expect(removeCLIColors(lastFrame())),
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const setupWebApp = () => {
|
|
64
|
+
const view = webRender(<MeasureWebApp />);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
closeWebApp: view.unmount,
|
|
68
|
+
expectWebAppToMatchSnapshot: (snapshotName: string) => matchSnapshot(view, snapshotName),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
test("it displays measures", async () => {
|
|
73
|
+
const { closeCli, expectCliOutput } = setupCli();
|
|
74
|
+
const { closeWebApp, expectWebAppToMatchSnapshot } = setupWebApp();
|
|
75
|
+
await expectWebAppToBeOpened();
|
|
76
|
+
|
|
77
|
+
expectCliOutput().toMatchInlineSnapshot(`
|
|
78
|
+
"
|
|
79
|
+
Flashlight web app running on: http://localhost:${DEFAULT_PORT}
|
|
80
|
+
"
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
// Autodetect app id com.example
|
|
84
|
+
await screen.findByText("Auto-Detect");
|
|
85
|
+
fireEvent.click(screen.getByText("Auto-Detect"));
|
|
86
|
+
await screen.findByDisplayValue("com.example");
|
|
87
|
+
|
|
88
|
+
// Start measuring
|
|
89
|
+
fireEvent.click(screen.getByText("Start Measuring"));
|
|
90
|
+
|
|
91
|
+
// Initial report screen with no measures
|
|
92
|
+
await screen.findByText("Average Test Runtime");
|
|
93
|
+
expectWebAppToMatchSnapshot("Web app with no measures yet");
|
|
94
|
+
|
|
95
|
+
// Simulate measures being emitted on the device
|
|
96
|
+
act(() => emitMeasures());
|
|
97
|
+
|
|
98
|
+
// We should now see 1000ms of measures: 3 measures at 0/500/1000ms
|
|
99
|
+
await screen.findByText("1000 ms");
|
|
100
|
+
// Find the score!
|
|
101
|
+
screen.getByText("47");
|
|
102
|
+
|
|
103
|
+
// expand threads
|
|
104
|
+
await screen.findByText("Other threads");
|
|
105
|
+
fireEvent.click(screen.getByText("Other threads"));
|
|
106
|
+
|
|
107
|
+
expectWebAppToMatchSnapshot("Web app with measures and threads opened");
|
|
108
|
+
|
|
109
|
+
// Stop measuring
|
|
110
|
+
fireEvent.click(screen.getByText("Stop Measuring"));
|
|
111
|
+
await waitFor(() => expect(aTraceMock.kill).toHaveBeenCalled());
|
|
112
|
+
await waitFor(() => expect(perfProfilerMock.kill).toHaveBeenCalled());
|
|
113
|
+
|
|
114
|
+
// Close apps
|
|
115
|
+
|
|
116
|
+
await closeCli();
|
|
117
|
+
closeWebApp();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("it handles the --port flag correctly", async () => {
|
|
121
|
+
const customPort = 1001;
|
|
122
|
+
|
|
123
|
+
const { closeCli, expectCliOutput } = setupCli(customPort);
|
|
124
|
+
|
|
125
|
+
const { closeWebApp } = setupWebApp();
|
|
126
|
+
|
|
127
|
+
const expectWebAppToBeOpenedOnCustomPort = () =>
|
|
128
|
+
waitFor(() => expect(open).toHaveBeenCalledWith(`http://localhost:${customPort}`));
|
|
129
|
+
await expectWebAppToBeOpenedOnCustomPort();
|
|
130
|
+
|
|
131
|
+
expectCliOutput().toMatchInlineSnapshot(`
|
|
132
|
+
"
|
|
133
|
+
Flashlight web app running on: http://localhost:${customPort}
|
|
134
|
+
"
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
// Close apps
|
|
138
|
+
await closeCli();
|
|
139
|
+
closeWebApp();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import supertest from "supertest";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
|
|
5
|
+
import { createExpressApp } from "../../server/ServerApp";
|
|
6
|
+
|
|
7
|
+
jest.mock("fs", () => ({
|
|
8
|
+
promises: {
|
|
9
|
+
readFile: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("ServerApp", () => {
|
|
14
|
+
let app: express.Express;
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
jest.spyOn(express, "static").mockImplementation(() => (req, res, next) => next());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const FLASHLIGHT_DATA_PLACEHOLDER =
|
|
21
|
+
'window.__FLASHLIGHT_DATA__ = { socketServerUrl: "http://localhost:4000" };';
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
(fs.promises.readFile as jest.Mock).mockResolvedValue(
|
|
25
|
+
`<html><script>${FLASHLIGHT_DATA_PLACEHOLDER}</script></html>`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
app = createExpressApp({
|
|
29
|
+
port: 9999,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("GET /", () => {
|
|
34
|
+
it("injects FlashlightData into index.html", async () => {
|
|
35
|
+
const response = await supertest(app).get("/");
|
|
36
|
+
|
|
37
|
+
expect(response.statusCode).toBe(200);
|
|
38
|
+
expect(response.text).toContain(
|
|
39
|
+
`window.__FLASHLIGHT_DATA__ = { socketServerUrl: "http://localhost:9999" };`
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("index.html contains the FlashlightData placeholder", async () => {
|
|
45
|
+
const fsPromises = jest.requireActual("fs").promises;
|
|
46
|
+
const fileContent = await fsPromises.readFile(`${__dirname}/../../webapp/index.html`, "utf8");
|
|
47
|
+
expect(fileContent).toContain(FLASHLIGHT_DATA_PLACEHOLDER);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// from https://stackoverflow.com/questions/17998978/removing-colors-from-output
|
|
2
|
+
// Remove colors so that snapshots are not polluted
|
|
3
|
+
export const removeCLIColors = (str?: string) =>
|
|
4
|
+
// eslint-disable-next-line no-control-regex
|
|
5
|
+
str?.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]/g, "");
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
|
|
3
|
+
jest.mock("socket.io-client", () => {
|
|
4
|
+
return {
|
|
5
|
+
...jest.requireActual("socket.io-client"),
|
|
6
|
+
io: jest.fn().mockImplementation(() => {
|
|
7
|
+
return {
|
|
8
|
+
on: jest.fn(),
|
|
9
|
+
close: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let originalWindow: Window & typeof globalThis;
|
|
16
|
+
|
|
17
|
+
describe("socket", () => {
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
originalWindow = global.window;
|
|
20
|
+
|
|
21
|
+
global.window = Object.create(window);
|
|
22
|
+
Object.defineProperty(window, "__FLASHLIGHT_DATA__", {
|
|
23
|
+
value: { socketServerUrl: "http://localhost:9999" },
|
|
24
|
+
writable: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
// Restore the original window object
|
|
30
|
+
global.window = originalWindow;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("sets the expected socket server URL", async () => {
|
|
34
|
+
await import("../../webapp/socket");
|
|
35
|
+
expect(io).toHaveBeenCalledWith("http://localhost:9999");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Logger } from "@clary-so/logger";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import type { Socket } from "socket.io";
|
|
4
|
+
import type { Socket as ClientSocket } from "socket.io-client";
|
|
5
|
+
|
|
6
|
+
export const useLogSocketEvents = (socket: Socket | ClientSocket) => {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
function onSocketEvent(event: string, ...args: unknown[]) {
|
|
9
|
+
Logger.debug(`Received socket event: ${event} with ${JSON.stringify(args)}`);
|
|
10
|
+
}
|
|
11
|
+
socket.onAny(onSocketEvent);
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
socket.offAny(onSocketEvent);
|
|
15
|
+
};
|
|
16
|
+
}, [socket]);
|
|
17
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import { promises as fs } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import cors from "cors";
|
|
6
|
+
import { Server } from "socket.io";
|
|
7
|
+
import { open } from "@clary-so/shell";
|
|
8
|
+
import React, { useEffect, useState } from "react";
|
|
9
|
+
import { SocketType, SocketServer } from "./socket/socketInterface";
|
|
10
|
+
import { HostAndPortInfo } from "./components/HostAndPortInfo";
|
|
11
|
+
import { getWebAppUrl } from "./constants";
|
|
12
|
+
import { ServerSocketConnectionApp } from "./ServerSocketConnectionApp";
|
|
13
|
+
import { render, useInput } from "ink";
|
|
14
|
+
import { profiler } from "@clary-so/profiler";
|
|
15
|
+
|
|
16
|
+
const pathToDist = path.join(__dirname, "../../dist");
|
|
17
|
+
|
|
18
|
+
export const createExpressApp = ({ port }: { port: number }) => {
|
|
19
|
+
const app = express();
|
|
20
|
+
app.use(cors({ origin: true }));
|
|
21
|
+
|
|
22
|
+
app.get("/", async (_, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const indexHtml = path.join(pathToDist, "index.html");
|
|
25
|
+
let data = await fs.readFile(indexHtml, "utf8");
|
|
26
|
+
data = data.replace("localhost:4000", `localhost:${port}`);
|
|
27
|
+
|
|
28
|
+
res.send(data);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
res.status(500).send("Error loading the page");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Serve the webapp folder built by parcel
|
|
35
|
+
app.use(express.static(pathToDist));
|
|
36
|
+
return app;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const allowOnlyOneSocketClient = (io: SocketServer, onConnect: (socket: SocketType) => void) => {
|
|
40
|
+
let currentSocketClient: SocketType | null = null;
|
|
41
|
+
io.on("connection", (socket) => {
|
|
42
|
+
currentSocketClient?.disconnect(true);
|
|
43
|
+
onConnect(socket);
|
|
44
|
+
currentSocketClient = socket;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const useCleanupOnManualExit = () => {
|
|
49
|
+
useInput(async (input) => {
|
|
50
|
+
switch (input) {
|
|
51
|
+
case "q":
|
|
52
|
+
case "c":
|
|
53
|
+
profiler.cleanup();
|
|
54
|
+
process.exit();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface ServerAppProps {
|
|
60
|
+
port: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const ServerApp = ({ port }: ServerAppProps) => {
|
|
64
|
+
const [socket, setSocket] = useState<SocketType | null>(null);
|
|
65
|
+
const webAppUrl = getWebAppUrl(port);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const app = createExpressApp({ port });
|
|
68
|
+
|
|
69
|
+
const server = http.createServer(app);
|
|
70
|
+
const io: SocketServer = new Server(server, {
|
|
71
|
+
cors: {
|
|
72
|
+
origin: [webAppUrl],
|
|
73
|
+
methods: ["GET", "POST"],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
allowOnlyOneSocketClient(io, setSocket);
|
|
78
|
+
|
|
79
|
+
server.listen(port, () => {
|
|
80
|
+
open(webAppUrl);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
server.close();
|
|
85
|
+
io.close();
|
|
86
|
+
};
|
|
87
|
+
}, [port, webAppUrl]);
|
|
88
|
+
useCleanupOnManualExit();
|
|
89
|
+
|
|
90
|
+
return socket ? (
|
|
91
|
+
<ServerSocketConnectionApp socket={socket} url={webAppUrl} />
|
|
92
|
+
) : (
|
|
93
|
+
<HostAndPortInfo url={webAppUrl} />
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const runServerApp = (port: number) => {
|
|
98
|
+
render(
|
|
99
|
+
<ServerApp port={port} />,
|
|
100
|
+
// handle it ourselves in the profiler to kill child processes thanks to useCleanupOnManualExit
|
|
101
|
+
{ exitOnCtrlC: false }
|
|
102
|
+
);
|
|
103
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { PerformanceMeasurer } from "@clary-so/e2e";
|
|
2
|
+
import { Logger } from "@clary-so/logger";
|
|
3
|
+
import { profiler } from "@clary-so/profiler";
|
|
4
|
+
import { Measure } from "@clary-so/types";
|
|
5
|
+
import React, { useCallback, useEffect } from "react";
|
|
6
|
+
import { HostAndPortInfo } from "./components/HostAndPortInfo";
|
|
7
|
+
import { SocketType, SocketEvents } from "./socket/socketInterface";
|
|
8
|
+
import { useSocketState, updateMeasuresReducer, addNewResultReducer } from "./socket/socketState";
|
|
9
|
+
import { useBundleIdControls } from "./useBundleIdControls";
|
|
10
|
+
import { useLogSocketEvents } from "../common/useLogSocketEvents";
|
|
11
|
+
|
|
12
|
+
export const ServerSocketConnectionApp = ({ socket, url }: { socket: SocketType; url: string }) => {
|
|
13
|
+
useLogSocketEvents(socket);
|
|
14
|
+
const [state, setState] = useSocketState(socket);
|
|
15
|
+
const performanceMeasureRef = React.useRef<PerformanceMeasurer | null>(null);
|
|
16
|
+
|
|
17
|
+
const stop = useCallback(async () => {
|
|
18
|
+
performanceMeasureRef.current?.forceStop();
|
|
19
|
+
setState({
|
|
20
|
+
isMeasuring: false,
|
|
21
|
+
});
|
|
22
|
+
}, [setState]);
|
|
23
|
+
|
|
24
|
+
useBundleIdControls(socket, setState, stop);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const updateMeasures = (measures: Measure[]) =>
|
|
28
|
+
setState((state) => updateMeasuresReducer(state, measures));
|
|
29
|
+
const addNewResult = (bundleId: string) =>
|
|
30
|
+
setState((state) =>
|
|
31
|
+
addNewResultReducer(
|
|
32
|
+
state,
|
|
33
|
+
`${bundleId}${state.results.length > 0 ? ` (${state.results.length + 1})` : ""}`,
|
|
34
|
+
profiler.detectDeviceRefreshRate()
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
socket.on(SocketEvents.START, async () => {
|
|
39
|
+
setState({
|
|
40
|
+
isMeasuring: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!state.bundleId) {
|
|
44
|
+
Logger.error("No bundle id provided");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
profiler.installProfilerOnDevice();
|
|
49
|
+
performanceMeasureRef.current = new PerformanceMeasurer(state.bundleId, {
|
|
50
|
+
recordOptions: {
|
|
51
|
+
record: false,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
addNewResult(state.bundleId);
|
|
56
|
+
performanceMeasureRef.current?.start(() =>
|
|
57
|
+
updateMeasures(performanceMeasureRef.current?.measures || [])
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
socket.on(SocketEvents.STOP, stop);
|
|
62
|
+
|
|
63
|
+
socket.on(SocketEvents.RESET, () => {
|
|
64
|
+
stop();
|
|
65
|
+
setState({
|
|
66
|
+
results: [],
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
socket.removeAllListeners(SocketEvents.START);
|
|
72
|
+
socket.removeAllListeners(SocketEvents.STOP);
|
|
73
|
+
socket.removeAllListeners(SocketEvents.RESET);
|
|
74
|
+
};
|
|
75
|
+
}, [setState, socket, state.bundleId, stop]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<HostAndPortInfo url={url} />
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import { DEFAULT_PORT } from "./constants";
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.command("measure")
|
|
8
|
+
.summary("Measure performance of an Android app")
|
|
9
|
+
.description(
|
|
10
|
+
`Measure performance of an Android app. Display the results live in a web app.
|
|
11
|
+
|
|
12
|
+
Main usage:
|
|
13
|
+
flashlight measure`
|
|
14
|
+
)
|
|
15
|
+
.option("-p, --port [port]", "Specify the port number for the server")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
const port = Number(options.port) || DEFAULT_PORT;
|
|
18
|
+
// measure command can be a bit slow to load since we run ink, express and socket.io, so lazy load it
|
|
19
|
+
const { runServerApp } = await import("./ServerApp");
|
|
20
|
+
runServerApp(port);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
program.parse();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
export const HostAndPortInfo = ({ url }: { url: string }) => (
|
|
5
|
+
<Box padding={1} flexDirection="column">
|
|
6
|
+
<Text>
|
|
7
|
+
<Text bold>Flashlight web app running on: </Text>
|
|
8
|
+
<Text color={"blue"}>{url}</Text>
|
|
9
|
+
</Text>
|
|
10
|
+
</Box>
|
|
11
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TestCaseResult } from "@clary-so/types";
|
|
2
|
+
import { Server, Socket } from "socket.io";
|
|
3
|
+
|
|
4
|
+
export interface SocketData {
|
|
5
|
+
isMeasuring: boolean;
|
|
6
|
+
bundleId: string | null;
|
|
7
|
+
results: TestCaseResult[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ServerToClientEvents {
|
|
11
|
+
updateState: (state: SocketData) => void;
|
|
12
|
+
sendError(error: unknown): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ClientToServerEvents {
|
|
16
|
+
start: () => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
autodetectBundleId: () => void;
|
|
20
|
+
setBundleId: (bundleId: string) => void;
|
|
21
|
+
autodetectRefreshRate: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface InterServerEvents {
|
|
25
|
+
ping: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SocketServer = Server<
|
|
29
|
+
ClientToServerEvents,
|
|
30
|
+
ServerToClientEvents,
|
|
31
|
+
InterServerEvents,
|
|
32
|
+
SocketData
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
export type SocketType = Socket<
|
|
36
|
+
ClientToServerEvents,
|
|
37
|
+
ServerToClientEvents,
|
|
38
|
+
InterServerEvents,
|
|
39
|
+
SocketData
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
export enum SocketEvents {
|
|
43
|
+
START = "start",
|
|
44
|
+
STOP = "stop",
|
|
45
|
+
RESET = "reset",
|
|
46
|
+
AUTODETECT_BUNDLE_ID = "autodetectBundleId",
|
|
47
|
+
SET_BUNDLE_ID = "setBundleId",
|
|
48
|
+
UPDATE_STATE = "updateState",
|
|
49
|
+
SEND_ERROR = "sendError",
|
|
50
|
+
PING = "ping",
|
|
51
|
+
CONNECT = "connect",
|
|
52
|
+
DISCONNECT = "disconnect",
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Measure, POLLING_INTERVAL } from "@clary-so/types";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { SocketType, SocketData, SocketEvents } from "./socketInterface";
|
|
4
|
+
|
|
5
|
+
export const useSocketState = (socket: SocketType) => {
|
|
6
|
+
const [state, _setState] = useState<SocketData>({
|
|
7
|
+
isMeasuring: false,
|
|
8
|
+
bundleId: null,
|
|
9
|
+
results: [],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const setState = (
|
|
13
|
+
newState: Partial<SocketData> | ((previousState: SocketData) => SocketData)
|
|
14
|
+
) => {
|
|
15
|
+
_setState(
|
|
16
|
+
typeof newState === "function"
|
|
17
|
+
? newState
|
|
18
|
+
: (previousState) => ({
|
|
19
|
+
...previousState,
|
|
20
|
+
...newState,
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
socket.emit(SocketEvents.UPDATE_STATE, state);
|
|
27
|
+
}, [state, socket]);
|
|
28
|
+
|
|
29
|
+
return [state, setState] as const;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const updateMeasuresReducer = (state: SocketData, measures: Measure[]): SocketData => ({
|
|
33
|
+
...state,
|
|
34
|
+
results: [
|
|
35
|
+
...state.results.slice(0, state.results.length - 1),
|
|
36
|
+
{
|
|
37
|
+
...state.results[state.results.length - 1],
|
|
38
|
+
iterations: [
|
|
39
|
+
{
|
|
40
|
+
measures,
|
|
41
|
+
time: (measures.length || 0) * POLLING_INTERVAL,
|
|
42
|
+
status: "SUCCESS",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const addNewResultReducer = (
|
|
50
|
+
state: SocketData,
|
|
51
|
+
name: string,
|
|
52
|
+
refreshRate: number
|
|
53
|
+
): SocketData => ({
|
|
54
|
+
...state,
|
|
55
|
+
results: [
|
|
56
|
+
...state.results,
|
|
57
|
+
{
|
|
58
|
+
name,
|
|
59
|
+
iterations: [],
|
|
60
|
+
status: "SUCCESS",
|
|
61
|
+
specs: {
|
|
62
|
+
refreshRate,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { profiler } from "@clary-so/profiler";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { SocketType, SocketData, SocketEvents } from "./socket/socketInterface";
|
|
4
|
+
|
|
5
|
+
export const useBundleIdControls = (
|
|
6
|
+
socket: SocketType,
|
|
7
|
+
setState: (state: Partial<SocketData>) => void,
|
|
8
|
+
stop: () => void
|
|
9
|
+
) => {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
socket.on(SocketEvents.SET_BUNDLE_ID, (bundleId) => {
|
|
12
|
+
setState({
|
|
13
|
+
bundleId,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
socket.on(SocketEvents.AUTODETECT_BUNDLE_ID, () => {
|
|
18
|
+
stop();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const bundleId = profiler.detectCurrentBundleId();
|
|
22
|
+
setState({
|
|
23
|
+
bundleId,
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
socket.emit(
|
|
27
|
+
SocketEvents.SEND_ERROR,
|
|
28
|
+
error instanceof Error ? error.message : "unknown error"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
socket.removeAllListeners(SocketEvents.SET_BUNDLE_ID);
|
|
35
|
+
socket.removeAllListeners(SocketEvents.AUTODETECT_BUNDLE_ID);
|
|
36
|
+
};
|
|
37
|
+
}, [setState, socket, stop]);
|
|
38
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
setThemeAtRandom,
|
|
5
|
+
IterationsReporterView,
|
|
6
|
+
getThemeColorPalette,
|
|
7
|
+
} from "@clary-so/web-reporter-ui";
|
|
8
|
+
|
|
9
|
+
import { BundleIdSelector } from "./components/BundleIdSelector";
|
|
10
|
+
import { StartButton } from "./components/StartButton";
|
|
11
|
+
import { Delete } from "@mui/icons-material";
|
|
12
|
+
import { AppBar } from "./components/AppBar";
|
|
13
|
+
import { useMeasures } from "./useMeasures";
|
|
14
|
+
import { SocketState } from "./components/SocketState";
|
|
15
|
+
|
|
16
|
+
setThemeAtRandom();
|
|
17
|
+
|
|
18
|
+
export const MeasureWebApp = () => {
|
|
19
|
+
const { autodetect, bundleId, start, stop, results, isMeasuring, reset, setBundleId } =
|
|
20
|
+
useMeasures();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="bg-light-charcoal h-full text-black">
|
|
24
|
+
<SocketState />
|
|
25
|
+
<AppBar>
|
|
26
|
+
<BundleIdSelector autodetect={autodetect} bundleId={bundleId} onChange={setBundleId} />
|
|
27
|
+
{bundleId ? (
|
|
28
|
+
<div className="flex flex-row gap-2">
|
|
29
|
+
<StartButton start={start} stop={stop} isMeasuring={isMeasuring} />
|
|
30
|
+
{/* It's assumed that the color palette is fixed randomly by setThemeAtRandom
|
|
31
|
+
and is an array of >= 4 colors */}
|
|
32
|
+
<div data-theme={getThemeColorPalette()[1]}>
|
|
33
|
+
<Button onClick={reset} icon={<Delete />}>
|
|
34
|
+
Reset
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
) : null}
|
|
39
|
+
</AppBar>
|
|
40
|
+
<IterationsReporterView results={results} />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|