Skip to content

Commit b4d619c

Browse files
authored
feat(performance statistics): gather performance statistics (#257)
1 parent 569b316 commit b4d619c

9 files changed

Lines changed: 106 additions & 46 deletions

File tree

scully/fileHanderPlugins/asciidoc.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,4 @@ const asciiDocPlugin = async (raw: string) => {
88
return asciidoctor.convert(raw, {});
99
};
1010

11-
registerPlugin('fileHandler', 'adoc', {
12-
alternateExtensions: ['asciidoc', 'asc'],
13-
handler: asciiDocPlugin,
14-
});
11+
registerPlugin('fileHandler', 'adoc', asciiDocPlugin, ['asciidoc', 'asc']);

scully/fileHanderPlugins/markdown.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,4 @@ const markdownPlugin = async (raw: string) => {
66
return marked(raw);
77
};
88

9-
registerPlugin('fileHandler', 'md', {
10-
alternateExtensions: ['markdown'],
11-
handler: markdownPlugin,
12-
});
9+
registerPlugin('fileHandler', 'md', markdownPlugin, ['markdown']);
Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
1+
import {performance} from 'perf_hooks';
12
import {HandledRoute} from '../routerPlugins/addOptionalRoutesPlugin';
23
import {logError, yellow} from '../utils/log';
4+
import {performanceIds} from '../utils/performanceIds';
35

46
// export const configValidator = Symbol('configValidator');
57
export const configValidator = `___Scully_Validate_config_plugin___`;
6-
7-
export type PluginHandler = (...args: any) => Promise<any>;
8-
export interface Plugin {
9-
handler: PluginHandler;
10-
}
8+
export const AlternateExtensionsForFilePlugin = Symbol('altfileextension');
119

1210
export type ErrorString = string;
1311
export type ConfigValidator = (HandledRoute) => ErrorString[];
1412

15-
export interface FilePlugin {
16-
alternateExtensions?: string[];
17-
handler: PluginHandler;
18-
}
13+
type RoutePlugin = (route: string, config: any) => Promise<HandledRoute[]>;
14+
type RenderPlugin = (html: string, route: HandledRoute) => Promise<string>;
15+
type FilePlugin = (html: string) => Promise<string>;
1916

2017
interface Plugins {
21-
render: {[html: string]: PluginHandler};
22-
router: {[path: string]: PluginHandler};
18+
render: {[name: string]: RenderPlugin};
19+
router: {[name: string]: RoutePlugin};
2320
fileHandler: {[fileExtension: string]: FilePlugin};
2421
}
2522

@@ -35,7 +32,7 @@ export const registerPlugin = (
3532
type: PluginTypes,
3633
name: string,
3734
plugin: any,
38-
validator = async (config?: any) => [],
35+
pluginOptions: any = async (config?: any) => [],
3936
{replaceExistingPlugin = false} = {}
4037
) => {
4138
if (!['router', 'render', 'fileHandler'].includes(type)) {
@@ -49,14 +46,42 @@ export const registerPlugin = (
4946
if (replaceExistingPlugin === false && plugins[type][name]) {
5047
throw new Error(`Plugin ${name} already exists`);
5148
}
52-
if (type === 'router' && typeof validator !== 'function') {
53-
logError(`
49+
if (type === 'router') {
50+
if (typeof pluginOptions !== 'function') {
51+
logError(`
5452
---------------
5553
Route plugin "${yellow(name)}" should have an config validator attached to '${plugin.name}'
5654
---------------
5755
`);
58-
plugin[configValidator] = async () => [];
56+
plugin[configValidator] = async () => [];
57+
} else {
58+
plugin[configValidator] = pluginOptions;
59+
}
5960
}
60-
plugin[configValidator] = validator;
61-
plugins[type][name] = plugin;
61+
if (type === 'fileHandler') {
62+
if (pluginOptions && Array.isArray(pluginOptions)) {
63+
plugin[AlternateExtensionsForFilePlugin] = pluginOptions;
64+
} else {
65+
plugin[AlternateExtensionsForFilePlugin] = [];
66+
}
67+
}
68+
plugins[type][name] = (...args) => wrap(type, name, plugin, args);
6269
};
70+
71+
async function wrap(type: string, name: string, plugin: (...args) => any | FilePlugin, args: any) {
72+
let id = `plugin-${type}:${name}-`;
73+
if (type === 'router') {
74+
id += args[0];
75+
}
76+
if (type === 'render') {
77+
id += args[1].route;
78+
}
79+
if (type === 'fileHandler') {
80+
plugin;
81+
}
82+
performance.mark('start' + id);
83+
const result = await plugin(...args);
84+
performance.mark('stop' + id);
85+
performanceIds.add(id);
86+
return result;
87+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import {FilePlugin, plugins} from '../../pluginManagement/pluginRepository';
1+
import {AlternateExtensionsForFilePlugin, plugins} from '../../pluginManagement/pluginRepository';
22
import {logError} from '../../utils/log';
33
export async function handleFile(extension: string, fileContent: string) {
44
extension = extension.trim().toLowerCase();
55
let plugin = plugins.fileHandler[extension];
66
if (!plugin) {
77
/** find by alternate extensions */
88
const t = Object.entries(plugins.fileHandler).find(
9-
([name, pl]: [string, FilePlugin]) =>
10-
pl.alternateExtensions && pl.alternateExtensions.includes(extension)
9+
([name, pl]: [string, () => any]) =>
10+
pl[AlternateExtensionsForFilePlugin] && pl[AlternateExtensionsForFilePlugin].includes(extension)
1111
);
1212
if (t.length) {
1313
plugin = t[1];
1414
} else {
1515
throw new logError(`unknown filetype ${extension}`);
1616
}
1717
}
18-
return plugin.handler(fileContent) as Promise<string>;
18+
return plugin(fileContent) as Promise<string>;
1919
}

scully/renderPlugins/contentRenderPlugin.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {handleFile} from './content-render-utils/handleFile';
66
import {insertContent} from './content-render-utils/insertContent';
77
import {readFileAndCheckPrePublishSlug} from './content-render-utils/readFileAndCheckPrePublishSlug';
88
import {JSDOM} from 'jsdom';
9-
import {nodeModuleNameResolver} from 'typescript';
109

1110
registerPlugin('render', 'contentFolder', contentRenderPlugin);
1211

scully/renderPlugins/launchedBrowser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function obsBrowser(options: LaunchOptions = scullyConfig.puppeteerLaunchOptions
1919
if (showBrowser) {
2020
options.headless = false;
2121
}
22-
// option.args= ['--no-sandbox', '--disable-setuid-sandbox'],
22+
// options.args = ['--no-sandbox', '--disable-setuid-sandbox'];
2323

2424
const {SCULLY_PUPPETEER_EXECUTABLE_PATH} = process.env;
2525
if (SCULLY_PUPPETEER_EXECUTABLE_PATH) {

scully/utils/defaultAction.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {cpus} from 'os';
2+
import {performance} from 'perf_hooks';
3+
import * as yargs from 'yargs';
24
import {launchedBrowser} from '../renderPlugins/launchedBrowser';
35
import {routeContentRenderer} from '../renderPlugins/routeContentRenderer';
46
import {addOptionalRoutes, HandledRoute} from '../routerPlugins/addOptionalRoutesPlugin';
@@ -10,8 +12,7 @@ import {chunk} from './chunk';
1012
import {loadConfig, updateScullyConfig} from './config';
1113
import {ScullyConfig} from './interfacesandenums';
1214
import {log, logWarn} from './log';
13-
14-
import * as yargs from 'yargs';
15+
import {performanceIds} from './performanceIds';
1516

1617
const {baseFilter} = yargs
1718
.string('bf')
@@ -27,17 +28,23 @@ export const generateAll = async (config?: Partial<ScullyConfig>, localBaseFilte
2728
await loadConfig;
2829
try {
2930
log('Finding all routes in application.');
31+
performance.mark('startTraverse');
3032
const unhandledRoutes = await traverseAppRoutes();
33+
performance.mark('stopTraverse');
34+
performanceIds.add('Traverse');
3135

3236
if (unhandledRoutes.length < 1) {
3337
logWarn('No routes found in application, are you sure you installed the router? Terminating.');
3438
process.exit(15);
3539
}
3640

41+
performance.mark('startDiscovery');
42+
performanceIds.add('Discovery');
3743
log('Pull in data to create additional routes.');
3844
const handledRoutes = await addOptionalRoutes(
3945
unhandledRoutes.filter((r: string) => r && r.startsWith(localBaseFilter))
4046
);
47+
performance.mark('stopDiscovery');
4148
/** save routerinfo, so its available during rendering */
4249
if (localBaseFilter === '') {
4350
/** only store when the routes are complete */
@@ -48,7 +55,10 @@ export const generateAll = async (config?: Partial<ScullyConfig>, localBaseFilte
4855
const browser = await launchedBrowser();
4956
/** start handling each route, works in chunked parallel mode */
5057
// await doChunks(handledRoutes);
58+
performance.mark('startRender');
59+
performanceIds.add('Render');
5160
await renderParallel(handledRoutes);
61+
performance.mark('stopRender');
5262
return handledRoutes;
5363
} catch (e) {
5464
// TODO: add better error handling

scully/utils/performanceIds.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* an constant to keep track of the id's for performance gathering
3+
*/
4+
export const performanceIds = new Set<string>();

scully/utils/startup.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {generateAll} from './defaultAction';
22
import {ScullyConfig} from './interfacesandenums';
3-
import {performance, PerformanceObserver} from 'perf_hooks';
3+
import {performance, PerformanceObserver, PerformanceObserverCallback} from 'perf_hooks';
44
import {log, yellow} from './log';
5+
import {performanceIds} from './performanceIds';
56

67
/**
78
* Starts the entire process
@@ -10,20 +11,23 @@ import {log, yellow} from './log';
1011
export const startScully = (config?: Partial<ScullyConfig>) => {
1112
let routeCount = 0;
1213
return new Promise(resolve => {
13-
performance.mark('start');
14-
const obs = new PerformanceObserver((list, observer) => {
15-
const duration = list.getEntries()[0].duration;
16-
performance.clearMarks();
17-
observer.disconnect();
18-
resolve({routeCount, duration});
19-
});
14+
performance.mark('startDuration');
15+
performanceIds.add('Duration');
16+
let innerResolve;
17+
const durationProm = new Promise(r => (innerResolve = r));
18+
const obs = new PerformanceObserver(measurePerformance(innerResolve));
2019
obs.observe({entryTypes: ['measure'], buffered: true});
21-
generateAll(config).then(routes => {
22-
routeCount = routes.length;
23-
performance.mark('stop');
24-
performance.measure('duration', 'start', 'stop');
20+
const numberOfRoutesProm = generateAll(config).then(routes => {
21+
performance.mark('stopDuration');
22+
/** measure all performance checks */
23+
[...performanceIds.values()].forEach(id => performance.measure(id, `start${id}`, `stop${id}`));
24+
return routes.length;
2525
});
26-
}).then(({routeCount: numberOfRoutes, duration}) => {
26+
Promise.all([numberOfRoutesProm, durationProm]).then(([numberOfRoutes, durations]) =>
27+
resolve({numberOfRoutes, durations})
28+
);
29+
}).then(({numberOfRoutes, durations}: {numberOfRoutes: number; durations: {[key: string]: number}}) => {
30+
const duration = durations.Duration;
2731
// tslint:disable-next-line:variable-name
2832
const seconds = duration / 1000;
2933
const routesProSecond = Math.ceil((numberOfRoutes / seconds) * 100) / 100;
@@ -32,6 +36,30 @@ export const startScully = (config?: Partial<ScullyConfig>) => {
3236
Generating took ${yellow(Math.floor(seconds * 100) / 100)} seconds for ${yellow(numberOfRoutes)} pages:
3337
That is ${yellow(routesProSecond)} pages per second,
3438
or ${yellow(Math.ceil(singleTime))} milliseconds for each page.
39+
40+
Finding routes in the angular app took ${logSeconds(durations.Traverse)}
41+
Pulling in route-data took ${logSeconds(durations.Discovery)}
42+
Rendering the pages took ${logSeconds(durations.Render)}
43+
3544
`);
3645
});
3746
};
47+
48+
function measurePerformance(resolve: (value?: unknown) => void): PerformanceObserverCallback {
49+
return (list, observer) => {
50+
const durations = list
51+
.getEntries()
52+
.reduce((acc, entry) => ({...acc, [entry.name]: Math.floor(entry.duration * 100) / 100}), {});
53+
// console.log(durations);
54+
performance.clearMarks();
55+
observer.disconnect();
56+
resolve(durations);
57+
};
58+
}
59+
60+
function logSeconds(milliSeconds) {
61+
if (milliSeconds < 1000) {
62+
return yellow(Math.floor(milliSeconds)) + ' milliseconds';
63+
}
64+
return yellow(Math.floor(milliSeconds / 10) / 100) + ' seconds';
65+
}

0 commit comments

Comments
 (0)