From 91dc56ecf5910a5d13ce499d12b16d3ec8989f05 Mon Sep 17 00:00:00 2001 From: Diego Ferreiro Val Date: Mon, 17 Jun 2019 17:01:20 -0700 Subject: [PATCH] fix: more types --- packages/@best/builder/tsconfig.json | 4 +- packages/@best/config/src/internal-types.ts | 5 + packages/@best/config/src/utils/defaults.ts | 2 +- .../@best/console-stream/src/build-stream.ts | 1 - .../@best/console-stream/src/runner-stream.ts | 178 +++++++++++++++--- packages/@best/runner-abstract/src/index.ts | 76 ++++---- packages/@best/runner-abstract/tsconfig.json | 7 +- packages/@best/runner/tsconfig.json | 7 +- 8 files changed, 220 insertions(+), 60 deletions(-) diff --git a/packages/@best/builder/tsconfig.json b/packages/@best/builder/tsconfig.json index 773ebe65..06bf5055 100644 --- a/packages/@best/builder/tsconfig.json +++ b/packages/@best/builder/tsconfig.json @@ -6,6 +6,8 @@ }, "references": [ { "path": "../messager" }, - { "path": "../runtime" } + { "path": "../runtime" }, + { "path": "../console-stream"}, + { "path": "../config" }, ] } diff --git a/packages/@best/config/src/internal-types.ts b/packages/@best/config/src/internal-types.ts index c7090c71..de6351b5 100644 --- a/packages/@best/config/src/internal-types.ts +++ b/packages/@best/config/src/internal-types.ts @@ -75,9 +75,14 @@ export interface GlobalConfig { export type ProjectConfigPlugin = string | [string, { [key : string]: any }] export interface ProjectConfig { + useHttp: boolean; benchmarkRunner: string; benchmarkRunnerConfig: any; benchmarkOutput: string; + benchmarkOnClient: boolean; + benchmarkMaxDuration: number; + benchmarkMinIterations: number; + benchmarkIterations: number; benchmarkCustomAssets: string; cacheDirectory: string; projectName: string; diff --git a/packages/@best/config/src/utils/defaults.ts b/packages/@best/config/src/utils/defaults.ts index 58c48c42..3b3752f6 100644 --- a/packages/@best/config/src/utils/defaults.ts +++ b/packages/@best/config/src/utils/defaults.ts @@ -4,7 +4,7 @@ const defaultOptions = { cache: true, gitIntegration: false, cacheDirectory: cacheDirectory(), - useHttp: true, + useHttp: false, openPages: false, moduleDirectories: ['node_modules'], moduleFileExtensions: ['js'], diff --git a/packages/@best/console-stream/src/build-stream.ts b/packages/@best/console-stream/src/build-stream.ts index 74f2bc22..5d414a0f 100644 --- a/packages/@best/console-stream/src/build-stream.ts +++ b/packages/@best/console-stream/src/build-stream.ts @@ -103,7 +103,6 @@ export default class BuildOutputStream { this.stdout.write(this.printBenchmark(benchmarkState)); } } - } scheduleUpdate() { diff --git a/packages/@best/console-stream/src/runner-stream.ts b/packages/@best/console-stream/src/runner-stream.ts index 23314318..147f420e 100644 --- a/packages/@best/console-stream/src/runner-stream.ts +++ b/packages/@best/console-stream/src/runner-stream.ts @@ -8,18 +8,52 @@ enum State { QUEUED = 'QUEUED', RUNNING = 'RUNNING', DONE = 'DONE', + ERROR = 'ERROR', +} +interface BuildConfig { + benchmarkName: string, + benchmarkFolder: string, + benchmarkSignature: string, + benchmarkEntry: string, + projectConfig: { projectName: string, rootDir: string }, + globalConfig: any, } -interface BenchmarkRunnerState { state: State; displayPath: string; projectName: string } -type AllBencharkRunnerState = Map +interface BenchmarkStatus { state: State; displayPath: string; projectName: string } +type AllBencharkRunnerState = Map + +interface RunnerConfig { + maxDuration: number; + minSampleCount: number, + iterations: number, + iterateOnClient: boolean +} + +interface RunnerState { + executedTime: number, + executedIterations: number, + results: any[], + iterateOnClient: boolean, +} +interface BenchmarkProgress { + executedIterations: number, + estimated: number, + runtime: number, + avgIteration: number, +} const STATE_ANSI = { - RUNNING: chalk.reset.inverse.yellow.bold(` ${State.RUNNING} `), + RUNNING: chalk.reset.inverse.yellow.bold(` ${State.RUNNING} `), QUEUED: chalk.reset.inverse.gray.bold(` ${State.QUEUED} `), - DONE: chalk.reset.inverse.green.bold(` ${State.DONE} `) + ERROR: chalk.reset.inverse.redBright.bold(` ${State.ERROR} `), + DONE: chalk.reset.inverse.green.bold(` ${State.DONE} `), + }; const INIT_MSG = '\n Running benchmarks... \n\n'; +const PROGRESS_TEXT = chalk.dim('Progress running: '); +const PROGRESS_BAR_WIDTH = 40; +const SCHEDULE_TIMEOUT = 50; function printState(state: State) { return STATE_ANSI[state]; @@ -35,21 +69,49 @@ function printProjectName(projectName: string) { return ' ' + chalk.reset.cyan.dim(`(${projectName})`); } -interface BuildConfig { - benchmarkName: string, - benchmarkFolder: string, - benchmarkSignature: string, - benchmarkEntry: string, - projectConfig: any, - globalConfig: any, +function calculateBenchmarkProgress(progress: RunnerState, { iterations, maxDuration }: RunnerConfig): BenchmarkProgress { + const { executedIterations, executedTime } = progress; + const avgIteration = executedTime / executedIterations; + const runtime = parseInt((executedTime / 1000) + '', 10); + const estimated = iterations ? Math.round(iterations * avgIteration / 1000) + 1 : maxDuration / 1000; + + return { + executedIterations, + estimated, + runtime, + avgIteration, + }; +} + +function printProgressBar(runTime: number, estimatedTime: number, width: number) { + // If we are more than one second over the estimated time, highlight it. + const renderedTime = + estimatedTime && runTime >= estimatedTime + 1 ? chalk.bold.yellow(runTime + 's') : runTime + 's'; + + let time = chalk.bold(`Time:`) + ` ${renderedTime}`; + if (runTime < estimatedTime) { + time += `, estimated ${estimatedTime}s`; + } + + // Only show a progress bar if the test run is actually going to take some time + if (estimatedTime > 2 && runTime < estimatedTime && width) { + const availableWidth = Math.min(PROGRESS_BAR_WIDTH, width); + const length = Math.min(Math.floor(runTime / estimatedTime * availableWidth), availableWidth); + if (availableWidth >= 2) { + time += '\n' + chalk.green('█').repeat(length) + chalk.white('█').repeat(availableWidth - length); + } + } + return time; } export default class BuildOutputStream { stdout: NodeJS.WriteStream; isInteractive: boolean; + _streamBuffer: string = ''; _state: AllBencharkRunnerState; _innerLog: string = ''; + _progress: BenchmarkProgress | null = null; _scheduled: NodeJS.Timeout | null = null; constructor(buildConfig: BuildConfig[], stream: NodeJS.WriteStream, isInteractive?: boolean) { @@ -58,8 +120,15 @@ export default class BuildOutputStream { this._state = this.initState(buildConfig); } - initState(buildConfig: BuildConfig[]) { - return buildConfig.reduce((state: AllBencharkRunnerState, build: any) => { + initState(buildConfigs: BuildConfig[]): AllBencharkRunnerState { + return buildConfigs.reduce((state: AllBencharkRunnerState, build: any): AllBencharkRunnerState => { + buildConfigs.forEach(({ benchmarkEntry, projectConfig: { projectName, rootDir }}) => { + state.set(benchmarkEntry, { + projectName, + state: State.QUEUED, + displayPath: benchmarkEntry, + }); + }); return state; }, new Map()); } @@ -84,9 +153,11 @@ export default class BuildOutputStream { updateRunnerState(benchmarkPath: string, state: State) { const stateConfig = this._state.get(benchmarkPath); if (!stateConfig) { - throw new Error(`Unknown benchmark build started (${benchmarkPath})`); + throw new Error(`Unknown benchmark build started (${benchmarkPath})`); + } + if (stateConfig.state !== State.ERROR) { + stateConfig.state = state; } - stateConfig.state = state; } scheduleUpdate() { @@ -94,11 +165,11 @@ export default class BuildOutputStream { this._scheduled = setTimeout(() => { this.updateStream(); this._scheduled = null; - }, 10); + }, SCHEDULE_TIMEOUT); } } - printRunner({ state, projectName, displayPath }:{ state: State, projectName: string, displayPath: string }) { + printBenchmarkState({ state, projectName, displayPath }: { state: State, projectName: string, displayPath: string }) { const columns = this.stdout.columns || 80; const overflow = columns - (state.length + projectName.length + displayPath.length + /* for padding */ 10); const hasOverflow = overflow < 0; @@ -110,15 +181,30 @@ export default class BuildOutputStream { return `${ansiState} ${ansiProjectName} ${ansiDisplayname}\n`; } + printProgress(progress: BenchmarkProgress, { displayPath }: BenchmarkStatus): string { + const benchmarkName = chalk.bold.black(path.basename(displayPath)); + return [ + `\n${PROGRESS_TEXT} ${benchmarkName}`, + chalk.bold.black('Avg iteration: ') + progress.avgIteration.toFixed(2) + 'ms', + chalk.bold.black('Completed iterations: ') + progress.executedIterations, + printProgressBar(progress.runtime, progress.estimated, 40) + ].join('\n') + '\n\n'; + } + updateStream() { - const innerState = this._innerLog; + const progress = this._progress; let buffer = INIT_MSG; - for (const { state, displayPath, projectName } of this._state.values()) { - buffer += this.printRunner({ state, displayPath, projectName }); + let current: BenchmarkStatus | undefined; + for (const benchmarkState of this._state.values()) { + const { state, displayPath, projectName } = benchmarkState; + buffer += this.printBenchmarkState({ state, displayPath, projectName }); + if (state === State.RUNNING) { + current = benchmarkState; + } } - if (innerState) { - buffer += `\n${innerState}\n`; + if (current && progress) { + buffer += this.printProgress(progress, current); } this.clearBufferStream(); @@ -134,6 +220,54 @@ export default class BuildOutputStream { } } + // -- Lifecycle + + onBenchmarkStart(benchmarkPath: string) { + this.updateRunnerState(benchmarkPath, State.RUNNING); + if (this.isInteractive) { + this.scheduleUpdate(); + } else { + const benchmarkState = this._state.get(benchmarkPath); + if (benchmarkState) { + this.stdout.write(this.printBenchmarkState(benchmarkState)); + } + } + } + + onBenchmarkEnd(benchmarkPath: string) { + this.updateRunnerState(benchmarkPath, State.DONE); + this._innerLog = ''; + + const benchmarkState = this._state.get(benchmarkPath); + if (benchmarkState) { + if (this.isInteractive) { + if (benchmarkState.state === State.ERROR) { + this.updateStream(); + this.stdout.write('\n'); + } else { + this.scheduleUpdate(); + } + } else { + this.stdout.write(this.printBenchmarkState(benchmarkState) + '\n'); + } + } + } + + onBenchmarkError(benchmarkPath: string) { + this.updateRunnerState(benchmarkPath, State.ERROR); + } + + updateBenchmarkProgress(state: RunnerState, runtimeOpts: RunnerConfig) { + const progress = calculateBenchmarkProgress(state, runtimeOpts); + this._progress = progress; + + if (this.isInteractive) { + this.scheduleUpdate(); + } else { + // WIP: Update for no sync + } + } + init() { if (this.isInteractive) { this.updateStream(); diff --git a/packages/@best/runner-abstract/src/index.ts b/packages/@best/runner-abstract/src/index.ts index 2455c0c3..f0d37375 100644 --- a/packages/@best/runner-abstract/src/index.ts +++ b/packages/@best/runner-abstract/src/index.ts @@ -1,16 +1,30 @@ import { getSystemInfo } from '@best/utils'; import express from 'express'; import { dirname, basename, join } from 'path'; -import { spawn } from 'child_process'; import { Socket } from 'net'; import { RunnerOutputStream } from "@best/console-stream"; +import { FrozenGlobalConfig, FrozenProjectConfig } from '@best/config'; interface RunnerBundle { benchmarkName: string, benchmarkEntry: string, benchmarkFolder: string, benchmarkSignature: string -}; +} + +interface RuntimeOptions { + maxDuration: number; + minSampleCount: number, + iterations: number, + iterateOnClient: boolean +} + +export interface BenchmarkResultsState { + executedTime: number, + executedIterations: number, + results: any[], + iterateOnClient: boolean, +} const UPDATE_INTERVAL = 300; @@ -20,32 +34,27 @@ export default abstract class AbstractRunner { page: any; browser: any; - async run({ benchmarkName, benchmarkEntry }: RunnerBundle, projectConfig: any, globalConfig: any, messager: RunnerOutputStream) { - const opts = this.normalizeRuntimeOptions(projectConfig); - const state = this.initializeBenchmarkState(opts); - const { projectName } = projectConfig; - const { openPages } = globalConfig; - const url = await this.runSetupAndGetUrl(benchmarkEntry, projectConfig); - - // Optionally open benchmarks in a browser for debugging. - const debugPages = openPages && /^de/i.test(process.env.NODE_ENV || ''); - if (debugPages) { - spawn('open', [url]); - } + async run({ benchmarkEntry }: RunnerBundle, projectConfig: FrozenProjectConfig, globalConfig: FrozenGlobalConfig, runnerLogStream: RunnerOutputStream) { + const { useHttp } = projectConfig; + const runtimeOptions = this.normalizeRuntimeOptions(projectConfig); + const state = this.initializeBenchmarkState(runtimeOptions); + const url = await this.runSetupAndGetUrl(benchmarkEntry, useHttp); try { await this.loadUrl(url, projectConfig); + const environment = await this.normalizeEnvironment(this.browserInfo, projectConfig, globalConfig); - messager.onBenchmarkStart(benchmarkName, projectName); - const { results } = await this.runIterations(this.page, state, opts, messager); + runnerLogStream.onBenchmarkStart(benchmarkEntry); + const { results } = await this.runIterations(this.page, state, runtimeOptions, runnerLogStream); return { results, environment }; + } catch (e) { - messager.onBenchmarkError(benchmarkName, projectName); + runnerLogStream.onBenchmarkError(benchmarkEntry); throw e; } finally { - messager.onBenchmarkEnd(benchmarkName, projectName); + runnerLogStream.onBenchmarkEnd(benchmarkEntry); this.closeBrowser(); - if (this.app && !debugPages) { + if (this.app) { this.app.stop(); } } @@ -66,16 +75,17 @@ export default abstract class AbstractRunner { runIteration(...args: any): any { throw new Error('runIteration() must be implemented'); } - runServerIterations(...args: any) { + runServerIterations(...args: any): Promise { throw new Error('runItrunServerIterationseration() must be implemented'); } - normalizeRuntimeOptions(projectConfig: any) { - const { benchmarkIterations, benchmarkOnClient } = projectConfig; + normalizeRuntimeOptions(projectConfig: FrozenProjectConfig): RuntimeOptions { + const { benchmarkIterations, benchmarkOnClient, benchmarkMaxDuration, benchmarkMinIterations } = projectConfig; const definedIterations = Number.isInteger(benchmarkIterations); + // For benchmarking on the client or a defined number of iterations duration is irrelevant - const maxDuration = definedIterations ? 1 : projectConfig.benchmarkMaxDuration; - const minSampleCount = definedIterations ? benchmarkIterations : projectConfig.benchmarkMinIterations; + const maxDuration = definedIterations ? 1 : benchmarkMaxDuration; + const minSampleCount = definedIterations ? benchmarkIterations : benchmarkMinIterations; return { maxDuration, @@ -85,16 +95,16 @@ export default abstract class AbstractRunner { }; } - initializeBenchmarkState(opts: any) { + initializeBenchmarkState({ iterateOnClient }: RuntimeOptions): BenchmarkResultsState { return { executedTime: 0, executedIterations: 0, results: [], - iterateOnClient: opts.iterateOnClient, + iterateOnClient, }; } - runSetupAndGetUrl(benchmarkEntry: string, { useHttp }: any): Promise { + runSetupAndGetUrl(benchmarkEntry: string, useHttp: boolean): Promise { if (!useHttp) { return Promise.resolve(`file://${benchmarkEntry}`); } @@ -164,13 +174,13 @@ export default abstract class AbstractRunner { }; } - async runIterations(page: any, state: any, opts: any, messager: any) { + async runIterations(page: any, state: BenchmarkResultsState, runtimeOptions: RuntimeOptions, runnnerLogStream: RunnerOutputStream) { return state.iterateOnClient - ? this.runClientIterations(page, state, opts, messager) - : this.runServerIterations(page, state, opts, messager); + ? this.runClientIterations(page, state, runtimeOptions, runnnerLogStream) + : this.runServerIterations(page, state, runtimeOptions, runnnerLogStream); } - async runClientIterations(page: any, state: any, opts: any, messager: any) { + async runClientIterations(page: any, state: BenchmarkResultsState, runtimeOptions: RuntimeOptions, runnerLogStream: RunnerOutputStream): Promise { // Run an iteration to estimate the time it will take const testResult = await this.runIteration(page, { iterations: 1 }); const estimatedIterationTime = testResult.executedTime; @@ -181,11 +191,11 @@ export default abstract class AbstractRunner { const executing = Date.now() - start; state.executedTime = executing; state.executedIterations = Math.round(executing / estimatedIterationTime); - messager.updateBenchmarkProgress(state, opts); + runnerLogStream.updateBenchmarkProgress(state, runtimeOptions); }, UPDATE_INTERVAL); await this.reloadPage(page); - const clientRawResults = await this.runIteration(page, opts); + const clientRawResults = await this.runIteration(page, runtimeOptions); clearInterval(intervalId); const results = clientRawResults.results; diff --git a/packages/@best/runner-abstract/tsconfig.json b/packages/@best/runner-abstract/tsconfig.json index b8635ca4..c43b6339 100644 --- a/packages/@best/runner-abstract/tsconfig.json +++ b/packages/@best/runner-abstract/tsconfig.json @@ -3,5 +3,10 @@ "compilerOptions": { "rootDir": "src", "outDir": "build", - } + }, + "references": [ + { "path": "../utils" }, + { "path": "../console-stream" }, + { "path": "../builder" }, + ] } diff --git a/packages/@best/runner/tsconfig.json b/packages/@best/runner/tsconfig.json index b8635ca4..29ba953f 100644 --- a/packages/@best/runner/tsconfig.json +++ b/packages/@best/runner/tsconfig.json @@ -3,5 +3,10 @@ "compilerOptions": { "rootDir": "src", "outDir": "build", - } + }, + "references": [ + { "path": "../builder" }, + { "path": "../console-stream" }, + { "path": "../runner-abstract" }, + ] }