Skip to content

Commit 74bcf7c

Browse files
committed
add complete (for now) reporter implementation
1 parent 1ec9acf commit 74bcf7c

File tree

9 files changed

+432
-106
lines changed

9 files changed

+432
-106
lines changed

incubator/reporter/eslint.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
module.exports = require("@rnx-kit/eslint-config");
1+
import config from "@rnx-kit/eslint-config";
2+
3+
// eslint-disable-next-line no-restricted-exports
4+
export default config;

incubator/reporter/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
"lint": "rnx-kit-scripts lint",
3535
"test": "rnx-kit-scripts test"
3636
},
37+
"dependencies": {
38+
"chalk": "^4.0.0"
39+
},
3740
"devDependencies": {
3841
"@rnx-kit/eslint-config": "*",
3942
"@rnx-kit/jest-preset": "*",

incubator/reporter/src/formatter.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import chalk from "chalk";
2+
import { stripVTControlCharacters } from "node:util";
3+
import type { Formatter } from "./types.js";
4+
5+
function identity<T>(x: T): T {
6+
return x;
7+
}
8+
9+
export const defaultFormatter: Formatter = {
10+
module: (moduleName: string) => formatModuleName(moduleName),
11+
path: (path: string) => chalk.blue(path),
12+
duration: (duration: number) => formatDuration(duration),
13+
task: (task: string) => chalk.bold(chalk.green(task)),
14+
action: (action: string) => chalk.cyan(action),
15+
reporter: (reporter: string) => chalk.bold(chalk.blue(reporter)),
16+
17+
// formatting functions for types of logging
18+
log: (text: string) => identity(text),
19+
error: (text: string) => chalk.red(`Error: `) + text,
20+
warn: (text: string) => chalk.yellow(`Warning: `) + text,
21+
verbose: (text: string) => chalk.dim(text),
22+
23+
// formatting helper for cleaning formatting
24+
clean: (msg: string) => stripVTControlCharacters(msg),
25+
};
26+
27+
function formatDuration(duration: number): string {
28+
if (duration > 1000) {
29+
return `${chalk.green((duration / 1000).toFixed(2))}s`;
30+
} else {
31+
const decimalPlaces = Math.max(0, 2 - Math.floor(Math.log10(duration)));
32+
return `${chalk.green(duration.toFixed(decimalPlaces))}ms`;
33+
}
34+
}
35+
36+
function formatModuleName(moduleName: string) {
37+
if (moduleName.startsWith("@")) {
38+
const parts = moduleName.split("/");
39+
if (parts.length > 1) {
40+
return chalk.bold(parts[0] + "/" + chalk.cyan(parts.slice(1).join("/")));
41+
}
42+
}
43+
return chalk.bold(chalk.cyan(moduleName));
44+
}

incubator/reporter/src/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1-
export const fakeMethod = (message: string) => {
2-
console.log(message);
3-
return message;
4-
};
1+
export {
2+
LOG_ALL,
3+
LOG_ERRORS,
4+
LOG_LOGS,
5+
LOG_NONE,
6+
LOG_VERBOSE,
7+
LOG_WARNINGS,
8+
} from "./constants.ts";
9+
export { defaultFormatter } from "./formatter.ts";
10+
export { enablePerformanceTracing } from "./performance.ts";
11+
export { createReporter } from "./reporter.ts";
12+
export { updateReportingDefaults } from "./reportingRoot.ts";
13+
export type {
14+
Formatter,
15+
Reporter,
16+
ReporterInfo,
17+
ReporterListener,
18+
ReporterOptions,
19+
} from "./types.ts";

incubator/reporter/src/performance.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { LOG_NONE, LOG_VERBOSE } from "./constants.ts";
2+
import { createReporter } from "./reporter.ts";
3+
import { ReportingRoot } from "./reportingRoot.ts";
4+
import type {
5+
ActionEvent,
6+
Formatter,
7+
Reporter,
8+
ReporterEvent,
9+
ReporterInfo,
10+
ReporterListener,
11+
TaskEvent,
12+
} from "./types.ts";
13+
14+
class PerformanceReporter {
15+
// Filter for reporters to monitor, undefined for all
16+
reporterFilter?: Set<string>;
17+
18+
// we don't care about messages, ignore them
19+
messageLevel: number = LOG_NONE;
20+
21+
//
22+
private taskDepth = 0;
23+
private reporter: Reporter;
24+
private format: Formatter;
25+
26+
private listener: ReporterListener = {
27+
messageLevel: LOG_NONE,
28+
acceptsSource: (source: ReporterInfo) =>
29+
!this.reporterFilter || this.reporterFilter.has(source.name),
30+
// eslint-disable-next-line @typescript-eslint/no-empty-function
31+
onMessage: () => {},
32+
onTaskStarted: (event: ReporterEvent) => this.onTaskStarted(event),
33+
onTaskCompleted: (event: TaskEvent) => this.onTaskCompleted(event),
34+
};
35+
36+
constructor(
37+
reporterFilter?: Set<string>,
38+
stdout?: NodeJS.WriteStream,
39+
stderr?: NodeJS.WriteStream
40+
) {
41+
this.reporterFilter = reporterFilter;
42+
this.reporter = createReporter("PerformanceReporter", {
43+
logLevel: LOG_VERBOSE,
44+
undecoratedOutput: true,
45+
stdout,
46+
stderr,
47+
});
48+
this.format = this.reporter.formatter;
49+
ReportingRoot.getInstance().addListener(this.listener);
50+
}
51+
52+
acceptsSource(source: ReporterInfo): boolean {
53+
return !this.reporterFilter || this.reporterFilter.has(source.name);
54+
}
55+
56+
/**
57+
* Called when a task is started
58+
*/
59+
onTaskStarted(event: ReporterEvent) {
60+
this.reporter.log(this.taskPrefix(event) + "Started");
61+
this.taskDepth++;
62+
}
63+
64+
/**
65+
* Called when a task is completed
66+
*/
67+
onTaskCompleted(event: TaskEvent) {
68+
const { duration } = this.reporter.formatter;
69+
this.taskDepth--;
70+
const taskPrefix = this.taskPrefix(event);
71+
72+
const actions = Object.values(event.actions);
73+
if (actions.length > 0) {
74+
// report a header for the actions if any are present
75+
this.reporter.log(`${taskPrefix} ${actions.length} action types logged:`);
76+
// now report each action found
77+
for (const action of actions) {
78+
this.reporter.log(
79+
`${this.actionPrefix(action)} calls in ${duration(action.elapsed)}`
80+
);
81+
}
82+
}
83+
// finish with reporting the task completed
84+
if (event.error) {
85+
this.reporter.log(
86+
`${taskPrefix} Failed (${duration(event.elapsed)}) with error: ${event.error.message}`
87+
);
88+
} else {
89+
this.reporter.log(`${taskPrefix} Completed (${duration(event.elapsed)}`);
90+
}
91+
}
92+
93+
private taskPrefix(event: ReporterEvent) {
94+
const source = this.format.reporter(event.source.name);
95+
const task = this.format.task(event.label);
96+
return `${"=".repeat(this.taskDepth)}> ${source}:${task}:`;
97+
}
98+
99+
private actionPrefix(event: ActionEvent) {
100+
const source = this.format.reporter(event.source.name);
101+
const action = this.format.action(event.label);
102+
const actionCount = String(event.calls).padStart(this.taskDepth + 6, " ");
103+
return `${actionCount} ${source}:${action}:`;
104+
}
105+
106+
finish() {
107+
ReportingRoot.getInstance().removeListener(this.listener);
108+
}
109+
}
110+
111+
export function enablePerformanceTracing(
112+
reporterFilter?: Set<string>,
113+
stdout?: NodeJS.WriteStream,
114+
stderr?: NodeJS.WriteStream
115+
) {
116+
const performanceReporter = new PerformanceReporter(
117+
reporterFilter,
118+
stdout,
119+
stderr
120+
);
121+
return {
122+
finish: () => {
123+
performanceReporter.finish();
124+
},
125+
};
126+
}

incubator/reporter/src/reporter.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
LOG_ERRORS,
3+
LOG_LOGS,
4+
LOG_VERBOSE,
5+
LOG_WARNINGS,
6+
} from "./constants.ts";
7+
import { ReportingRoot } from "./reportingRoot.ts";
8+
import type { Reporter, ReporterOptions } from "./types.ts";
9+
10+
/**
11+
* @param name the name of the reporter, ideally unique within the application or it could cause confusion
12+
* @param options optional configuration for the reporter, used to override default settings
13+
* @returns a new reporter instance
14+
*/
15+
export function createReporter(
16+
name: string,
17+
options: Partial<Omit<ReporterOptions, "name">> = {}
18+
): Reporter {
19+
const root = ReportingRoot.getInstance();
20+
const sourceInfo: ReporterOptions = {
21+
...root.reporterDefaults,
22+
...options,
23+
name,
24+
};
25+
const { logLevel, undecoratedOutput, formatter, stdout, stderr } = sourceInfo;
26+
27+
const stdPrefix = undecoratedOutput ? "" : `${formatter.reporter(name)}: `;
28+
const errorPrefix = formatter.error(stdPrefix);
29+
const warnPrefix = formatter.warn(stdPrefix);
30+
31+
const handleMsg = (
32+
level: number,
33+
msg: string,
34+
prefix: string,
35+
stream: NodeJS.WriteStream
36+
) => {
37+
if (level >= logLevel) {
38+
stream.write(`${prefix}${msg}\n`);
39+
}
40+
root.notifyMsg(level, msg, sourceInfo);
41+
};
42+
43+
return {
44+
error: (msg: string) => handleMsg(LOG_ERRORS, msg, errorPrefix, stderr),
45+
warn: (msg: string) => handleMsg(LOG_WARNINGS, msg, warnPrefix, stderr),
46+
log: (msg: string) => handleMsg(LOG_LOGS, msg, stdPrefix, stdout),
47+
verbose: (msg: string) =>
48+
handleMsg(LOG_VERBOSE, formatter.verbose(msg), stdPrefix, stdout),
49+
50+
// tasks are hierarchical operations that can be timed and tracked
51+
task: function <T>(label: string, fn: () => T) {
52+
return root.task<T>(label, sourceInfo, fn);
53+
},
54+
asyncTask: async function <T>(label: string, fn: () => Promise<T>) {
55+
return await root.asyncTask<T>(label, sourceInfo, fn);
56+
},
57+
58+
// action is a timed operation that can be executed within a task. Each action is tracked as part of the task
59+
action: function <T>(label: string, fn: () => T) {
60+
return root.action<T>(label, sourceInfo, fn);
61+
},
62+
asyncAction: async function <T>(label: string, fn: () => Promise<T>) {
63+
return await root.asyncAction<T>(label, sourceInfo, fn);
64+
},
65+
66+
formatter,
67+
};
68+
}

0 commit comments

Comments
 (0)