Skip to content

Commit

Permalink
feat(performance-alerts): WIP (#51)
Browse files Browse the repository at this point in the history
* feat: add timer around each metric

* chore: add ts migration metrics

* feat: warn if metrics take longer

* ci: add comment
  • Loading branch information
fwuensche authored Jul 1, 2024
1 parent 2492320 commit 6c9ae5f
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 19 deletions.
14 changes: 12 additions & 2 deletions .cherry.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const JS_FILES = 'app/**/*.{js,jsx}'
const TS_FILES = 'app/**/*.{ts,tsx}'
const JS_FILES = '**/*.{js,jsx}'
const TS_FILES = '**/*.{ts,tsx}'

module.exports = {
project_name: 'fwuensche/cherry-cli',
Expand All @@ -15,5 +15,15 @@ module.exports = {
name: 'TODO',
pattern: /TODO/,
},
{
name: '[TS Migration] TS lines of code',
include: TS_FILES,
groupByFile: true,
},
{
name: '[TS Migration] JS lines of code',
include: JS_FILES,
groupByFile: true,
},
],
}
2 changes: 2 additions & 0 deletions .github/workflows/cherry_diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ jobs:
run: npm install

- name: Raise if new JavaScript code is added
# This command will fail if the number of lines of code in JavaScript files has increased
# in the current branch compared to the base branch, encouraging developers to contribute to migrating to TS.
run: ./bin/cherry.js diff --metric='[loc] JavaScript' --error-if-increase --quiet
6 changes: 6 additions & 0 deletions src/helpers/console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const YELLOW = '\x1B[33m'
const RESET = '\x1B[0m'

export function warn(message) {
console.warn(`${YELLOW}⚠️ ${message}${RESET}`)
}
37 changes: 37 additions & 0 deletions src/helpers/timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { warn } from './console.js'

let timers = {}

/**
* Executes a provided function block and measures its execution time.
* Logs a message if the execution time exceeds 2 seconds.
*
* @param {Function} codeBlock - The block of code to execute.
* @returns {*} The result of the executed code block.
*/
export async function executeWithTiming(codeBlock, identifier) {
const startTime = performance.now()

const result = await codeBlock()

const endTime = performance.now()
const executionTime = endTime - startTime

timers[identifier] = executionTime

return result
}

/**
* Logs a warning for each long running task.
* A task is considered long running if it takes longer than the provided time limit.
*
* @param {number} timeLimitInMs - The time limit in milliseconds.
*/
export function warnsAboutLongRunningTasks(timeLimitInMs) {
for (const [identifier, executionTime] of Object.entries(timers).sort()) {
if (executionTime > timeLimitInMs) {
warn(`${identifier} took ${Math.round(executionTime)}ms`)
}
}
}
45 changes: 28 additions & 17 deletions src/occurrences.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import loc from './plugins/loc.js'
import npmOutdated from './plugins/npm_outdated.js'
import rubocop from './plugins/rubocop.js'
import yarnOutdated from './plugins/yarn_outdated.js'
import { executeWithTiming, warnsAboutLongRunningTasks } from './helpers/timer.js'

const spinnies = new Spinnies()

Expand Down Expand Up @@ -85,9 +86,14 @@ const matchPatterns = (files, metrics, quiet) => {
if (!files.length || !metrics.length) return []

if (!quiet) spinnies.add('patterns', { text: 'Matching patterns...', indent: 2 })

// Limit number of concurrently opened files to avoid "Error: spawn EBADF"
const limit = pLimit(10)
const promise = Promise.all(files.map((file) => limit(() => findFileOccurences(file, metrics))))
const promise = executeWithTiming(
() => Promise.all(files.map((file) => limit(() => findFileOccurences(file, metrics)))),
'All pattern metrics together'
)

if (!quiet) promise.then(() => spinnies.succeed('patterns', { text: 'Matching patterns' }))

return promise
Expand All @@ -97,17 +103,22 @@ const runEvals = (metrics, codeOwners, quiet) => {
if (!metrics.length) return []

if (!quiet) spinnies.add('evals', { text: 'Running eval()...', indent: 2 })

const promise = Promise.all(
metrics.map(async (metric) => {
if (!quiet)
if (!quiet) {
spinnies.add(`metric_${metric.name}`, {
text: `${metric.name}...`,
indent: 4,
})
const result = (await metric.eval({ codeOwners })).map((occurrence) => ({
...occurrence,
metricName: metric.name,
}))
}

const occurrences = await executeWithTiming(
async () => await metric.eval({ codeOwners }),
`Metric '${metric.name}'`
)
const result = occurrences.map((occurrence) => ({ ...occurrence, metricName: metric.name }))

if (!quiet) spinnies.succeed(`metric_${metric.name}`, { text: metric.name })
return result
})
Expand All @@ -126,7 +137,7 @@ const runPlugins = async (plugins, quiet) => {
const plugin = PLUGINS[name]
if (!plugin) panic(`Unsupported '${name}' plugin\nExpected one of: ${Object.keys(PLUGINS).join(', ')}`)
if (!quiet) spinnies.add(`plugin_${name}`, { text: `${name}...`, indent: 4 })
const result = await plugin.run(options)
const result = executeWithTiming(async () => await plugin.run(options), `Plugin '${name}'`)
if (!quiet) spinnies.succeed(`plugin_${name}`, { text: name })
return result
})
Expand Down Expand Up @@ -157,21 +168,21 @@ export const findOccurrences = async ({ configuration, files, metric, codeOwners
// From ['loc'] to { 'loc': {} } to handle deprecated array configuration for plugins
if (Array.isArray(plugins)) plugins = plugins.reduce((acc, value) => ({ ...acc, [value]: {} }), {})

const promise = Promise.all([
const result = await Promise.all([
matchPatterns(files, fileMetrics, quiet),
runEvals(evalMetrics, codeOwners, quiet),
runPlugins(plugins, quiet),
])

const occurrences = _.flattenDeep(await promise).map(
({ text, value, metricName, filePath, lineNumber, url, owners }) => ({
text,
value,
metricName,
url: url !== undefined ? url : filePath && buildPermalink(configuration.project_name, filePath, lineNumber),
owners: owners !== undefined ? owners : filePath && codeOwners.getOwners(filePath),
})
)
warnsAboutLongRunningTasks(5000)

const occurrences = _.flattenDeep(result).map(({ text, value, metricName, filePath, lineNumber, url, owners }) => ({
text,
value,
metricName,
url: url !== undefined ? url : filePath && buildPermalink(configuration.project_name, filePath, lineNumber),
owners: owners !== undefined ? owners : filePath && codeOwners.getOwners(filePath),
}))

return withEmptyMetrics(occurrences, metrics)
}

0 comments on commit 6c9ae5f

Please sign in to comment.