Skip to content

Commit

Permalink
feat: added profile-project command (#5780)
Browse files Browse the repository at this point in the history
<!--  Thanks for sending a pull request! Here are some tips for you:

1. If this is your first pull request, please read our contributor
guidelines in the
https://github.com/garden-io/garden/blob/main/CONTRIBUTING.md file.
2. Please label this pull request according to what type of issue you
are addressing (see "What type of PR is this?" below)
3. Ensure you have added or run the appropriate tests for your PR.
4. If the PR is unfinished, add `WIP:` at the beginning of the title or
use the GitHub Draft PR feature.
5. Please add at least two reviewers to the PR. Currently active
maintainers are: @edvald, @thsig, @eysi09, @stefreak, @TimBeyer, and
@vvagaytsev.
-->

**What this PR does / why we need it**:

Added a simple utility command for profiling projects, the
`profile-project` command.

This command logs the number of files included in each action and module
in the project (omitting actions that were converted from modules to
avoid duplication), their include/exclude configs (if any), and finally
the number of modules, actions and tracked files in the project.

This should be useful when helping users with large projects diagnose
slow init performance and how to best structure their includes, excludes
and `.gardenignore`.
  • Loading branch information
thsig committed Feb 27, 2024
1 parent 8b94e49 commit c83f815
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 12 deletions.
107 changes: 107 additions & 0 deletions core/src/commands/util/profile-project.ts
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2018-2023 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import type { CommandParams, CommandResult } from "../base.js"
import { Command } from "../base.js"
import { printEmoji, printHeader, renderDivider } from "../../logger/util.js"
import { dedent } from "../../util/string.js"
import { styles } from "../../logger/styles.js"
import type { ConfigGraph } from "../../graph/config-graph.js"
import indentString from "indent-string"
import type { BaseActionConfig } from "../../actions/types.js"

export class ProfileProjectCommand extends Command {
name = "profile-project"
help = "Renders a high-level sumamry of actions and modules in your project."
emoji = "📊"

override description = dedent`
Useful for diagnosing slow init performance for projects with lots of actions and modules and/or lots of files.
`

override printHeader({ log }) {
printHeader(log, "Profile Project", "️📊")
}

async action({ garden, log }: CommandParams): Promise<CommandResult> {
const graph = await garden.getConfigGraph({ log, emit: false })
summarizeGraph(log, garden, graph)

log.info(renderDivider())
log.info("Summary")
log.info("")
log.info("Module config count: " + styles.highlight(Object.keys(graph.moduleGraph.getModules()).length))
const actionConfigCount = Object.values(graph.getActions()).filter(
(a) => a.getInternal().moduleName === undefined
).length
log.info("Action config count (excluding those converted from modules): " + styles.highlight(actionConfigCount))
const trackedFilesInProjectRoot = await garden.vcs.getFiles({
log,
path: garden.projectRoot,
pathDescription: `project root`,
scanRoot: garden.projectRoot,
})
log.info("Total tracked files in project root:" + styles.highlight(trackedFilesInProjectRoot.length))
log.info("")
log.info(styles.success("OK") + " " + printEmoji("✔️", log))

return {}
}
}

function summarizeGraph(log: CommandParams["log"], garden: CommandParams["garden"], graph: ConfigGraph) {
if (Object.keys(graph.moduleGraph).length > 0) {
summarizeModuleGraph(log, graph.moduleGraph)
}
summarizeActionGraph(log, graph)
}

const indent = 2

function summarizeModuleGraph(log: CommandParams["log"], moduleGraph: ConfigGraph["moduleGraph"]) {
const sortedModules = Object.values(moduleGraph.getModules()).sort(
// We sort the modules by path and then name, so that modules at the same path appear together.
(m1, m2) => m1.path.localeCompare(m2.path) || m1.name.localeCompare(m2.name)
)
for (const module of sortedModules) {
log.info("Module: " + styles.highlight(module.name) + styles.primary(" (at " + module.path + ")"))
if (module.include && module.include.length > 0) {
log.info(indentString(styles.primary("Include: " + JSON.stringify(module.include, null, 2)), indent))
}
if (module.exclude && module.exclude.length > 0) {
log.info(indentString(styles.primary(" Exclude: " + module.exclude), indent))
}

log.info(indentString(styles.primary("Tracked file count: ") + module.version.files.length, indent))
log.info("")
}
}

function summarizeActionGraph(log: CommandParams["log"], graph: ConfigGraph) {
const sortedActions = Object.values(graph.getActions())
// We sort the actions by path and then name, so that actions at the same path appear together.
.sort((a1, a2) => a1.sourcePath().localeCompare(a2.sourcePath()) || a1.name.localeCompare(a2.name))
// We only want to show actions that are not converted from modules (since the file scanning cost for converted
// actions was incurred when scanning files for their parent module).
.filter((a) => a.getInternal().moduleName === undefined)
for (const action of sortedActions) {
const { include, exclude } = action._config as BaseActionConfig
log.info("Action: " + styles.highlight(action.name) + styles.primary(" (at " + action.sourcePath() + ")"))
if (action.getInternal().moduleName) {
log.info(indentString(styles.primary("From module: " + action.getInternal().moduleName), indent))
}
if (include && include.length > 0) {
log.info(indentString(styles.primary("Include: " + JSON.stringify(include, null, 2)), indent))
}
if (exclude && exclude.length > 0) {
log.info(indentString(styles.primary(" Exclude: " + exclude), indent))
}
log.info(indentString(styles.primary("Tracked file count: ") + action.getFullVersion().files.length, indent))
log.info("")
}
}
3 changes: 2 additions & 1 deletion core/src/commands/util/util.ts
Expand Up @@ -10,10 +10,11 @@ import { CommandGroup } from "../base.js"
import { FetchToolsCommand } from "./fetch-tools.js"
import { HideWarningCommand } from "./hide-warning.js"
import { MutagenCommand } from "./mutagen.js"
import { ProfileProjectCommand } from "./profile-project.js"

export class UtilCommand extends CommandGroup {
name = "util"
help = "Misc utility commands."

subCommands = [FetchToolsCommand, HideWarningCommand, MutagenCommand]
subCommands = [FetchToolsCommand, HideWarningCommand, MutagenCommand, ProfileProjectCommand]
}
37 changes: 27 additions & 10 deletions core/src/vcs/git-repo.ts
Expand Up @@ -53,6 +53,7 @@ const getIncludeExcludeFiles: IncludeExcludeFilesHandler<GitRepoGetFilesParams,
return { include, exclude, augmentedIncludes, augmentedExcludes }
}

// @Profile()
export class GitRepoHandler extends GitHandler {
override name = "git-repo"

Expand Down Expand Up @@ -112,14 +113,36 @@ export class GitRepoHandler extends GitHandler {
const filesAtPath = fileTree.getFilesAtPath(path)

log.debug(
`Found ${filesAtPath.length} files in path, filtering by ${augmentedIncludes.length} include and ${augmentedExcludes.length} exclude globs`
`Found ${filesAtPath.length} files in path ${path}, filtering by ${augmentedIncludes.length} include and ${augmentedExcludes.length} exclude globs`
)
log.silly(() => `Include globs: ${augmentedIncludes.join(", ")}`)
log.silly(() =>
log.debug(() => `Include globs: ${augmentedIncludes.join(", ")}`)
log.debug(() =>
augmentedExcludes.length > 0 ? `Exclude globs: ${augmentedExcludes.join(", ")}` : "No exclude globs"
)

const filtered = filesAtPath.filter(({ path: p }) => {
const filtered = this.filterPaths({ files: filesAtPath, log, path, augmentedIncludes, augmentedExcludes, filter })
log.debug(`Found ${filtered.length} files in path ${path} after glob matching`)
this.cache.set(log, filteredFilesCacheKey, filtered, pathToCacheContext(path))

return filtered
}

filterPaths({
log,
files,
path,
augmentedIncludes,
augmentedExcludes,
filter,
}: {
log: GetFilesParams["log"]
files: VcsFile[]
path: string
augmentedIncludes: string[]
augmentedExcludes: string[]
filter: GetFilesParams["filter"]
}): VcsFile[] {
return files.filter(({ path: p }) => {
if (filter && !filter(p)) {
return false
}
Expand All @@ -131,12 +154,6 @@ export class GitRepoHandler extends GitHandler {
log.silly(() => `Checking if ${relativePath} matches include/exclude globs`)
return matchPath(relativePath, augmentedIncludes, augmentedExcludes)
})

log.debug(`Found ${filtered.length} files in module path after glob matching`)

this.cache.set(log, filteredFilesCacheKey, filtered, pathToCacheContext(path))

return filtered
}

/**
Expand Down
12 changes: 12 additions & 0 deletions docs/reference/commands.md
Expand Up @@ -5852,6 +5852,18 @@ Examples:



### garden util profile-project

**Renders a high-level sumamry of actions and modules in your project.**

Useful for diagnosing slow init performance for projects with lots of actions and modules and/or lots of files.

#### Usage

garden util profile-project



### garden validate

**Check your garden configuration for errors.**
Expand Down
1 change: 0 additions & 1 deletion docs/reference/dockerhub-containers.md
Expand Up @@ -19,7 +19,6 @@ For your convenience, we build and publish Docker containers that contain the Ga
| [`gardendev/garden-gcloud`](https://hub.docker.com/r/gardendev/garden-gcloud) | Contains the Garden CLI, and the Google Cloud CLI |
| [`gardendev/garden-aws-gcloud`](https://hub.docker.com/r/gardendev/garden-aws-gcloud) | Contains the Garden CLI, the Google Cloud CLI and the AWS CLI v2 |
| [`gardendev/garden-aws-gcloud-azure`](https://hub.docker.com/r/gardendev/garden-aws-gcloud-azure) | Contains the Garden CLI, the Google Cloud CLI, the AWS CLI v2, and the Azure CLI |
| [`gardendev/garden-full`](https://hub.docker.com/r/gardendev/garden-full) | DEPRECATED: This container image is not being maintained anymore and will be removed in the future. |

### Tags

Expand Down

0 comments on commit c83f815

Please sign in to comment.