Skip to content

Commit

Permalink
feat(ios): have cpu usage per thread ios (#150)
Browse files Browse the repository at this point in the history
* feat(writeReport): add the details of cpu usage per thread

* chore(averageTestCaseResult): detect RN on iOS app + select correct default thread in report

* chore(webReporter): remove useless conditional check

* chore(writeReport): refacto to setup thread names in function of their id
  • Loading branch information
GuillaumeEgret committed Sep 12, 2023
1 parent c48bc3b commit ef2636c
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 37 deletions.
93 changes: 62 additions & 31 deletions packages/ios-poc/src/writeReport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { XMLParser } from "fast-xml-parser";
import fs from "fs";
import { Measure, TestCaseIterationResult, TestCaseResult } from "@perf-profiler/types";
import { Result, Row, isRefField } from "./utils/xmlTypes";
import { CpuMeasure, Measure, TestCaseIterationResult, TestCaseResult } from "@perf-profiler/types";
import { Result, Row, Thread, isRefField } from "./utils/xmlTypes";

export const writeReport = (inputFileName: string, outputFileName: string) => {
const xml = fs.readFileSync(inputFileName, "utf8");
Expand All @@ -11,28 +11,57 @@ export const writeReport = (inputFileName: string, outputFileName: string) => {
const TIME_INTERVAL = 500;
const NANOSEC_TO_MILLISEC = 1_000_000;
const CPU_TIME_INTERVAL = 10;
let lastSampleTimeInterval = 0;

const getMeasures = (row: Row[]) => {
const initThreadMap = (row: Row[]): { [id: number]: string } => {
const threadRef: { [id: number]: Thread } = {};
row.forEach((row: Row) => {
if (!isRefField(row.thread)) {
threadRef[row.thread.id] = row.thread;
}
});
return Object.values(threadRef).reduce((acc: { [id: number]: string }, thread) => {
const currentThreadName = thread.fmt
.split(" ")
.slice(0, thread.fmt.split(" ").indexOf(""))
.join(" ");
const currentTid = thread.tid.value;
const numberOfThread = Object.values(threadRef).filter((thread: Thread) => {
return thread.fmt.includes(currentThreadName) && thread.tid.value < currentTid;
}).length;
acc[thread.id] =
numberOfThread > 0 ? `${currentThreadName} (${numberOfThread})` : currentThreadName;
return acc;
}, {});
};

const getMeasures = (row: Row[]): Map<number, Map<string, number>> => {
const sampleTimeRef: { [id: number]: number } = {};
const classifiedMeasures = row.reduce((acc: Map<number, number>, row: Row) => {
const threadRef: { [id: number]: string } = initThreadMap(row);
const classifiedMeasures = row.reduce((acc: Map<number, Map<string, number>>, row: Row) => {
const sampleTime = isRefField(row.sampleTime)
? sampleTimeRef[row.sampleTime.ref]
: row.sampleTime.value / NANOSEC_TO_MILLISEC;
if (!isRefField(row.sampleTime)) {
sampleTimeRef[row.sampleTime.id] = sampleTime;
}

const correspondingTimeInterval = parseInt((sampleTime / TIME_INTERVAL).toFixed(0), 10);
lastSampleTimeInterval =
correspondingTimeInterval > lastSampleTimeInterval
? correspondingTimeInterval
: lastSampleTimeInterval;
const numberOfPointsIn = acc.get(correspondingTimeInterval) ?? 0;
acc.set(correspondingTimeInterval, numberOfPointsIn + 1);
const threadName = isRefField(row.thread)
? threadRef[row.thread.ref]
: threadRef[row.thread.id];

const correspondingTimeInterval =
parseInt((sampleTime / TIME_INTERVAL).toFixed(0), 10) * TIME_INTERVAL;

const timeIntervalMap = acc.get(correspondingTimeInterval) ?? new Map<string, number>();

const numberOfPointsIn = timeIntervalMap.get(threadName) ?? 0;

timeIntervalMap.set(threadName, numberOfPointsIn + 1);

acc.set(correspondingTimeInterval, timeIntervalMap);

return acc;
}, new Map<number, number>());
//return fillWithZeros(lastSampleTimeInterval, classifiedMeasures);
}, new Map<number, Map<string, number>>());
return classifiedMeasures;
};

Expand Down Expand Up @@ -61,28 +90,30 @@ export const writeReport = (inputFileName: string, outputFileName: string) => {
throw new Error("No rows in the xml file");
}

const measures: Map<number, number> = getMeasures(jsonObject.result.node.row);
const averagedMeasures: Measure[] = Array.from(measures.entries()).reduce(
(acc: Measure[], classifiedMeasures: [number, number]) => {
acc.push({
cpu: {
perName: {
total: (classifiedMeasures[1] * 10) / (TIME_INTERVAL / CPU_TIME_INTERVAL),
},
perCore: {},
},
const measures: Map<number, Map<string, number>> = getMeasures(jsonObject.result.node.row);
const formattedMeasures: Measure[] = Array.from(measures.entries()).map(
(classifiedMeasures: [number, Map<string, number>]) => {
const timeInterval = classifiedMeasures[0];
const timeIntervalMap = classifiedMeasures[1];
const cpuMeasure: CpuMeasure = {
perName: {},
perCore: {},
};
timeIntervalMap.forEach((value: number, key: string) => {
cpuMeasure.perName[key] = (value * 10) / (TIME_INTERVAL / CPU_TIME_INTERVAL);
});
return {
cpu: cpuMeasure,
ram: FAKE_RAM,
fps: FAKE_FPS,
time: classifiedMeasures[0],
});
return acc;
},
[]
time: timeInterval,
};
}
);

iterations.push({
time: averagedMeasures[averagedMeasures.length - 1].time,
measures: averagedMeasures,
time: formattedMeasures[formattedMeasures.length - 1].time,
measures: formattedMeasures,
status: "SUCCESS",
});

Expand Down
5 changes: 4 additions & 1 deletion packages/reporter/src/reporting/averageIterations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TestCaseIterationResult,
TestCaseResult,
ThreadNames,
ThreadNamesIOS,
} from "@perf-profiler/types";
import { mapValues } from "lodash";
import { getHighCpuUsageStats } from "./reporting";
Expand Down Expand Up @@ -67,7 +68,9 @@ export const averageTestCaseResult = (result: TestCaseResult): AveragedTestCaseR
average: averagedIterations,
averageHighCpuUsage: averageHighCpuUsage(result.iterations),
reactNativeDetected: averagedIterations.measures.some((measure) =>
Object.keys(measure.cpu.perName).some((key) => key === ThreadNames.JS_THREAD)
Object.keys(measure.cpu.perName).some(
(key) => key === ThreadNames.JS_THREAD || key === ThreadNamesIOS.JS_THREAD
)
),
};
};
10 changes: 9 additions & 1 deletion packages/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ export interface TestCaseIterationResult {
}

export type TestCaseResultStatus = "SUCCESS" | "FAILURE"; // Todo: add "SUCCESS_WITH_SOME_ITERATIONS_FAILED"

type TestCaseResultType = "IOS_EXPERIMENTAL" | undefined;
export interface TestCaseResult {
name: string;
score?: number;
status: TestCaseResultStatus;
iterations: TestCaseIterationResult[];
type?: undefined | "IOS_EXPERIMENTAL";
type?: TestCaseResultType;
}

export interface AveragedTestCaseResult {
Expand All @@ -45,6 +47,7 @@ export interface AveragedTestCaseResult {
average: TestCaseIterationResult;
averageHighCpuUsage: { [processName: string]: number };
reactNativeDetected: boolean;
type?: TestCaseResultType;
}

// Shouldn't really be here but @perf-profiler/types is imported by everyone and doesn't contain any logic
Expand All @@ -55,3 +58,8 @@ export const ThreadNames = {
UI_THREAD: "UI Thread",
JS_THREAD: "mqt_js",
};

export const ThreadNamesIOS = {
UI_THREAD: "Main Thread",
JS_THREAD: "com.facebook.react.JavaScript",
};
8 changes: 7 additions & 1 deletion packages/web-reporter-ui/src/sections/CPUReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Measure,
POLLING_INTERVAL,
ThreadNames,
ThreadNamesIOS,
} from "@perf-profiler/types";
import { getAverageCpuUsage } from "@perf-profiler/reporter";
import { Chart } from "../components/Chart";
Expand Down Expand Up @@ -32,8 +33,13 @@ const perThreadCpuAnnotationInterval = [{ y: 90, y2: 100, color: "#E62E2E", labe

export const CPUReport = ({ results }: { results: AveragedTestCaseResult[] }) => {
const reactNativeDetected = results.every((result) => result.reactNativeDetected);

const platformThreadNames = results.every((result) => result.type === "IOS_EXPERIMENTAL")
? ThreadNamesIOS
: ThreadNames;

const [selectedThreads, setSelectedThreads] = React.useState<string[]>(
reactNativeDetected ? [ThreadNames.JS_THREAD] : [ThreadNames.UI_THREAD]
reactNativeDetected ? [platformThreadNames.JS_THREAD] : [platformThreadNames.UI_THREAD]
);

const threads = selectedThreads
Expand Down
4 changes: 1 addition & 3 deletions packages/web-reporter/writeReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ export const writeReport = ({

const isIOSTestCaseResult = results.every((result) => result.type === "IOS_EXPERIMENTAL");

const report = JSON.stringify(
isIOSTestCaseResult ? results : getMeasuresForTimeInterval({ results, skip, duration })
);
const report = JSON.stringify(getMeasuresForTimeInterval({ results, skip, duration }));

const jsFileContent = fs.readFileSync(`${__dirname}/${scriptName}`, "utf8").replace(
// See App.tsx for the reason why we do this
Expand Down

0 comments on commit ef2636c

Please sign in to comment.