Skip to content

Commit 07b27db

Browse files
committed
refactor(release): split android performance artifact formatting
1 parent 4261a91 commit 07b27db

4 files changed

Lines changed: 181 additions & 131 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import path from "node:path";
2+
3+
const parseFirstNumber = (text, pattern) => {
4+
const match = text.match(pattern);
5+
return match ? Number(match[1].replaceAll(",", "")) : undefined;
6+
};
7+
8+
export const parseGfxinfo = (text) => ({
9+
frameDeadlineMissed: parseFirstNumber(
10+
text,
11+
/Frame deadline missed:\s+([\d,]+)/
12+
),
13+
jankyFrames: parseFirstNumber(text, /Janky frames:\s+([\d,]+)/),
14+
p50Ms: parseFirstNumber(text, /50th percentile:\s+([\d.]+)ms/),
15+
p90Ms: parseFirstNumber(text, /90th percentile:\s+([\d.]+)ms/),
16+
p95Ms: parseFirstNumber(text, /95th percentile:\s+([\d.]+)ms/),
17+
p99Ms: parseFirstNumber(text, /99th percentile:\s+([\d.]+)ms/),
18+
totalFrames: parseFirstNumber(text, /Total frames rendered:\s+([\d,]+)/)
19+
});
20+
21+
export const parseMeminfo = (text) => ({
22+
nativeHeapPssKb: parseFirstNumber(text, /Native Heap\s+([\d,]+)/),
23+
totalPssKb: parseFirstNumber(text, /TOTAL PSS:\s+([\d,]+)/),
24+
totalRssKb: parseFirstNumber(text, /TOTAL RSS:\s+([\d,]+)/)
25+
});
26+
27+
export const parseLaunchOutput = (text) => ({
28+
totalTimeMs: parseFirstNumber(text, /TotalTime:\s+([\d,]+)/),
29+
waitTimeMs: parseFirstNumber(text, /WaitTime:\s+([\d,]+)/)
30+
});
31+
32+
const formatMetric = (value, suffix = "") =>
33+
Number.isFinite(value) ? `${value.toLocaleString("en-US")}${suffix}` : "n/a";
34+
35+
export const createAndroidPerformanceMarkdown = ({
36+
androidPackage,
37+
commandText,
38+
commands,
39+
commit,
40+
deviceInfo,
41+
gfxinfoSummary,
42+
launchSummary,
43+
launchOutput,
44+
meminfoBeforeSummary,
45+
meminfoSummary,
46+
packageVersion,
47+
row,
48+
screenshotPath
49+
}) =>
50+
[
51+
`# ${row.id} Android Performance Sample`,
52+
"",
53+
`Date: ${new Date().toISOString().slice(0, 10)}`,
54+
`Commit: \`${commit || "n/a"}\``,
55+
`Package version: \`${packageVersion || "n/a"}\``,
56+
"Platform: Android emulator",
57+
`Build: release APK, \`${androidPackage}\``,
58+
`Renderer: ${row.renderer ?? "svg"} through React Native SVG`,
59+
`Scenario: ${row.target}`,
60+
`Showcase story: \`${row.showcaseStoryId}\``,
61+
`Deep link: \`${row.launchUrl}\``,
62+
"",
63+
"Expected fixture:",
64+
"",
65+
`- Chart type: ${row.expectedStoryMetrics?.chartType ?? "n/a"}`,
66+
`- Total points: ${formatMetric(row.expectedStoryMetrics?.totalPoints)}`,
67+
`- Visible points: ${formatMetric(row.expectedStoryMetrics?.visiblePoints)}`,
68+
`- Series count: ${formatMetric(row.expectedStoryMetrics?.seriesCount)}`,
69+
"",
70+
"Device:",
71+
"",
72+
`- Model: ${deviceInfo.model || "n/a"}`,
73+
`- Android: ${deviceInfo.androidVersion || "n/a"}`,
74+
`- Screen: ${deviceInfo.screenSize || "n/a"}`,
75+
"",
76+
"Commands used:",
77+
"",
78+
"```sh",
79+
...commands.map((item) => commandText(item.command, item.args)),
80+
"```",
81+
"",
82+
"Launch output:",
83+
"",
84+
"```text",
85+
launchOutput.trim() || "n/a",
86+
"```",
87+
"",
88+
"Launch timing:",
89+
"",
90+
"| Metric | Result |",
91+
"| --- | ---: |",
92+
`| TotalTime | ${formatMetric(launchSummary.totalTimeMs, " ms")} |`,
93+
`| WaitTime | ${formatMetric(launchSummary.waitTimeMs, " ms")} |`,
94+
"",
95+
"Frame timing:",
96+
"",
97+
"| Metric | Result |",
98+
"| --- | ---: |",
99+
`| Total frames rendered | ${formatMetric(gfxinfoSummary.totalFrames)} |`,
100+
`| Janky frames | ${formatMetric(gfxinfoSummary.jankyFrames)} |`,
101+
`| p50 frame time | ${formatMetric(gfxinfoSummary.p50Ms, " ms")} |`,
102+
`| p90 frame time | ${formatMetric(gfxinfoSummary.p90Ms, " ms")} |`,
103+
`| p95 frame time | ${formatMetric(gfxinfoSummary.p95Ms, " ms")} |`,
104+
`| p99 frame time | ${formatMetric(gfxinfoSummary.p99Ms, " ms")} |`,
105+
`| Frame deadline missed | ${formatMetric(gfxinfoSummary.frameDeadlineMissed)} |`,
106+
"",
107+
"Memory:",
108+
"",
109+
"| Metric | Before scenario | After scenario |",
110+
"| --- | ---: | ---: |",
111+
`| Total PSS | ${formatMetric(
112+
meminfoBeforeSummary.totalPssKb,
113+
" KB"
114+
)} | ${formatMetric(meminfoSummary.totalPssKb, " KB")} |`,
115+
`| Total RSS | ${formatMetric(
116+
meminfoBeforeSummary.totalRssKb,
117+
" KB"
118+
)} | ${formatMetric(meminfoSummary.totalRssKb, " KB")} |`,
119+
`| Native heap PSS | ${formatMetric(
120+
meminfoBeforeSummary.nativeHeapPssKb,
121+
" KB"
122+
)} | ${formatMetric(meminfoSummary.nativeHeapPssKb, " KB")} |`,
123+
"",
124+
"Artifact:",
125+
"",
126+
`- Screenshot: [${path.basename(screenshotPath)}](${path.basename(screenshotPath)})`,
127+
"",
128+
"Notes:",
129+
"",
130+
"- This is Android release-emulator evidence for one native performance matrix row.",
131+
"- It does not replace physical-device performance, iOS performance, Skia parity, or manual visible-correctness review.",
132+
""
133+
].join("\n");

scripts/capture-android-performance-evidence.mjs

Lines changed: 39 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import { mkdir, writeFile } from "node:fs/promises";
1+
import { mkdir, readFile, writeFile } from "node:fs/promises";
22
import path from "node:path";
33
import process from "node:process";
44
import { spawnSync } from "node:child_process";
55
import { fileURLToPath } from "node:url";
66

7+
import {
8+
createAndroidPerformanceMarkdown,
9+
parseGfxinfo,
10+
parseLaunchOutput,
11+
parseMeminfo
12+
} from "./android-performance-evidence-format.mjs";
713
import { listNativeQaRows } from "./record-native-qa-evidence.mjs";
814

915
const defaultAndroidPackage = "io.chartkit.showcase";
@@ -120,6 +126,29 @@ const runCommand = ({ args, command, encoding = "utf8" }) => {
120126
return result.stdout;
121127
};
122128

129+
const getGitCommit = (runner) => {
130+
try {
131+
return runner({
132+
args: ["rev-parse", "--short", "HEAD"],
133+
command: "git"
134+
}).trim();
135+
} catch {
136+
return "n/a";
137+
}
138+
};
139+
140+
const readPackageVersion = async (repoRoot) => {
141+
try {
142+
const packageJson = JSON.parse(
143+
await readFile(path.join(repoRoot, "package.json"), "utf8")
144+
);
145+
146+
return packageJson.version ?? "n/a";
147+
} catch {
148+
return "n/a";
149+
}
150+
};
151+
123152
const adbArgs = (device, args) => [...(device ? ["-s", device] : []), ...args];
124153

125154
const isLaunchOnlyScenario = (rowId) =>
@@ -166,133 +195,6 @@ const findRow = async ({ repoRoot, rowId }) => {
166195
return row;
167196
};
168197

169-
const parseFirstNumber = (text, pattern) => {
170-
const match = text.match(pattern);
171-
return match ? Number(match[1].replaceAll(",", "")) : undefined;
172-
};
173-
174-
const parseGfxinfo = (text) => ({
175-
frameDeadlineMissed: parseFirstNumber(
176-
text,
177-
/Frame deadline missed:\s+([\d,]+)/
178-
),
179-
jankyFrames: parseFirstNumber(text, /Janky frames:\s+([\d,]+)/),
180-
p50Ms: parseFirstNumber(text, /50th percentile:\s+([\d.]+)ms/),
181-
p90Ms: parseFirstNumber(text, /90th percentile:\s+([\d.]+)ms/),
182-
p95Ms: parseFirstNumber(text, /95th percentile:\s+([\d.]+)ms/),
183-
p99Ms: parseFirstNumber(text, /99th percentile:\s+([\d.]+)ms/),
184-
totalFrames: parseFirstNumber(text, /Total frames rendered:\s+([\d,]+)/)
185-
});
186-
187-
const parseMeminfo = (text) => ({
188-
nativeHeapPssKb: parseFirstNumber(text, /Native Heap\s+([\d,]+)/),
189-
totalPssKb: parseFirstNumber(text, /TOTAL PSS:\s+([\d,]+)/),
190-
totalRssKb: parseFirstNumber(text, /TOTAL RSS:\s+([\d,]+)/)
191-
});
192-
193-
const parseLaunchOutput = (text) => ({
194-
totalTimeMs: parseFirstNumber(text, /TotalTime:\s+([\d,]+)/),
195-
waitTimeMs: parseFirstNumber(text, /WaitTime:\s+([\d,]+)/)
196-
});
197-
198-
const formatMetric = (value, suffix = "") =>
199-
Number.isFinite(value) ? `${value.toLocaleString("en-US")}${suffix}` : "n/a";
200-
201-
const createMarkdown = ({
202-
commands,
203-
deviceInfo,
204-
gfxinfoSummary,
205-
launchSummary,
206-
launchOutput,
207-
meminfoBeforeSummary,
208-
meminfoSummary,
209-
row,
210-
screenshotPath,
211-
androidPackage
212-
}) =>
213-
[
214-
`# ${row.id} Android Performance Sample`,
215-
"",
216-
`Date: ${new Date().toISOString().slice(0, 10)}`,
217-
`Platform: Android emulator`,
218-
`Build: release APK, \`${androidPackage}\``,
219-
`Renderer: ${row.renderer ?? "svg"} through React Native SVG`,
220-
`Scenario: ${row.target}`,
221-
`Showcase story: \`${row.showcaseStoryId}\``,
222-
`Deep link: \`${row.launchUrl}\``,
223-
"",
224-
"Expected fixture:",
225-
"",
226-
`- Chart type: ${row.expectedStoryMetrics?.chartType ?? "n/a"}`,
227-
`- Total points: ${formatMetric(row.expectedStoryMetrics?.totalPoints)}`,
228-
`- Visible points: ${formatMetric(row.expectedStoryMetrics?.visiblePoints)}`,
229-
`- Series count: ${formatMetric(row.expectedStoryMetrics?.seriesCount)}`,
230-
"",
231-
"Device:",
232-
"",
233-
`- Model: ${deviceInfo.model || "n/a"}`,
234-
`- Android: ${deviceInfo.androidVersion || "n/a"}`,
235-
`- Screen: ${deviceInfo.screenSize || "n/a"}`,
236-
"",
237-
"Commands used:",
238-
"",
239-
"```sh",
240-
...commands.map((item) => commandText(item.command, item.args)),
241-
"```",
242-
"",
243-
"Launch output:",
244-
"",
245-
"```text",
246-
launchOutput.trim() || "n/a",
247-
"```",
248-
"",
249-
"Launch timing:",
250-
"",
251-
"| Metric | Result |",
252-
"| --- | ---: |",
253-
`| TotalTime | ${formatMetric(launchSummary.totalTimeMs, " ms")} |`,
254-
`| WaitTime | ${formatMetric(launchSummary.waitTimeMs, " ms")} |`,
255-
"",
256-
"Frame timing:",
257-
"",
258-
"| Metric | Result |",
259-
"| --- | ---: |",
260-
`| Total frames rendered | ${formatMetric(gfxinfoSummary.totalFrames)} |`,
261-
`| Janky frames | ${formatMetric(gfxinfoSummary.jankyFrames)} |`,
262-
`| p50 frame time | ${formatMetric(gfxinfoSummary.p50Ms, " ms")} |`,
263-
`| p90 frame time | ${formatMetric(gfxinfoSummary.p90Ms, " ms")} |`,
264-
`| p95 frame time | ${formatMetric(gfxinfoSummary.p95Ms, " ms")} |`,
265-
`| p99 frame time | ${formatMetric(gfxinfoSummary.p99Ms, " ms")} |`,
266-
`| Frame deadline missed | ${formatMetric(gfxinfoSummary.frameDeadlineMissed)} |`,
267-
"",
268-
"Memory:",
269-
"",
270-
"| Metric | Before scenario | After scenario |",
271-
"| --- | ---: | ---: |",
272-
`| Total PSS | ${formatMetric(
273-
meminfoBeforeSummary.totalPssKb,
274-
" KB"
275-
)} | ${formatMetric(meminfoSummary.totalPssKb, " KB")} |`,
276-
`| Total RSS | ${formatMetric(
277-
meminfoBeforeSummary.totalRssKb,
278-
" KB"
279-
)} | ${formatMetric(meminfoSummary.totalRssKb, " KB")} |`,
280-
`| Native heap PSS | ${formatMetric(
281-
meminfoBeforeSummary.nativeHeapPssKb,
282-
" KB"
283-
)} | ${formatMetric(meminfoSummary.nativeHeapPssKb, " KB")} |`,
284-
"",
285-
"Artifact:",
286-
"",
287-
`- Screenshot: [${path.basename(screenshotPath)}](${path.basename(screenshotPath)})`,
288-
"",
289-
"Notes:",
290-
"",
291-
"- This is Android release-emulator evidence for one native performance matrix row.",
292-
"- It does not replace physical-device performance, iOS performance, Skia parity, or manual visible-correctness review.",
293-
""
294-
].join("\n");
295-
296198
export const createAndroidPerformancePlan = async ({
297199
androidPackage = defaultAndroidPackage,
298200
device,
@@ -416,6 +318,9 @@ export const captureAndroidPerformanceEvidence = async ({
416318
const gfxinfo = runner(gfxCommand);
417319
const meminfo = runner(memCommand);
418320
const screenshot = runner({ ...screenshotCommand, encoding: "buffer" });
321+
const repoRoot = options.repoRoot ?? defaultRepoRoot;
322+
const commit = getGitCommit(runner);
323+
const packageVersion = await readPackageVersion(repoRoot);
419324
const deviceInfo = {
420325
androidVersion: runner({
421326
args: adbArgs(options.device, [
@@ -440,17 +345,20 @@ export const captureAndroidPerformanceEvidence = async ({
440345
await writeFile(plan.absoluteScreenshotPath, screenshot);
441346
await writeFile(
442347
plan.absoluteOutputPath,
443-
createMarkdown({
348+
createAndroidPerformanceMarkdown({
349+
androidPackage: plan.androidPackage,
350+
commandText,
444351
commands: plan.commands,
352+
commit,
445353
deviceInfo,
446354
gfxinfoSummary: parseGfxinfo(gfxinfo),
447355
launchSummary: parseLaunchOutput(launchOutput),
448356
launchOutput,
449357
meminfoBeforeSummary: parseMeminfo(meminfoBefore),
450358
meminfoSummary: parseMeminfo(meminfo),
359+
packageVersion,
451360
row: plan.row,
452-
screenshotPath: plan.screenshotPath,
453-
androidPackage: plan.androidPackage
361+
screenshotPath: plan.screenshotPath
454362
}),
455363
"utf8"
456364
);

scripts/capture-android-performance-evidence.test.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const createTempRepo = async () => {
1515
const evidenceDir = join(tempRepo, "docs/release/evidence");
1616

1717
await mkdir(evidenceDir, { recursive: true });
18+
await writeFile(
19+
join(tempRepo, "package.json"),
20+
`${JSON.stringify({ version: "7.0.0-test.0" }, null, 2)}\n`,
21+
"utf8"
22+
);
1823
await writeFile(
1924
join(evidenceDir, "native-performance-matrix.json"),
2025
`${JSON.stringify(
@@ -165,6 +170,7 @@ describe("Android performance evidence capture", () => {
165170
runner: (command) => {
166171
calls.push(command);
167172

173+
if (command.command === "git") return "abc1234\n";
168174
if (command.encoding === "buffer") return Buffer.from("png-bytes");
169175
if (command.args.includes("ro.build.version.release")) return "36\n";
170176
if (command.args.includes("ro.product.model")) return "Pixel Test\n";
@@ -201,6 +207,8 @@ describe("Android performance evidence capture", () => {
201207
);
202208

203209
expect(screenshot).toBe("png-bytes");
210+
expect(markdown).toContain("Commit: `abc1234`");
211+
expect(markdown).toContain("Package version: `7.0.0-test.0`");
204212
expect(markdown).toContain("Build: release APK, `io.test.showcase`");
205213
expect(markdown).toContain("| TotalTime | 812 ms |");
206214
expect(markdown).toContain("| p95 frame time | 18 ms |");

scripts/release-gate-config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const requiredFiles = [
4040
"scripts/record-native-qa-evidence.mjs",
4141
"scripts/capture-native-qa-screenshot.mjs",
4242
"scripts/capture-android-performance-evidence.mjs",
43+
"scripts/android-performance-evidence-format.mjs",
4344
"packages/core/package.json",
4445
"packages/react-native/package.json",
4546
"packages/svg-renderer/package.json",

0 commit comments

Comments
 (0)