Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for CLI plugin hooks #4376

Merged
merged 1 commit into from Mar 22, 2024

Conversation

laurazard
Copy link
Member

@laurazard laurazard commented Jun 26, 2023

- What I did

Introduce a "hooks" mechanism for CLI plugins, which enables CLI plugins adding useful, contextual messages/actions after a command has been executed.

This works by defining a special plugin command (such as the metadata command) which the CLI calls with some information about the just-executed command (currently, command and flag names). The plugin then prints a json-encoded to it's stdout with a template for the message it would like the CLI to print.

Using such a system, a plugin to help new docker users could, for example, declare intent to hook into docker build, and return a templated message instructing the user how to run their newly-built image.

A templating approach was chosen to allow plugin messages to include information from the original command execution (such as the tag used for the image in build), so that messages can provide more useful information (such as printing "Run your image with docker run my-image" after a docker build -t my-image .
Helpers are provided in the form of manager.TemplateReplaceFlagValue("tag") for this case, and use Go Templates under the hood.

The templating approach chosen also keeps sensitive data from leaving the CLI process.

This PR also introduces a "features" map into the CLI config.json, to allow for global enabling/disabling of this (and future) features.

Example Plugin
func main() {
	plugin.Run(func(dockerCli command.Cli) *cobra.Command {
		cmd := RootCommand(dockerCli)
		originalPreRun := cmd.PersistentPreRunE
		cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
			if err := plugin.PersistentPreRunE(cmd, args); err != nil {
				//nolint: wrapcheck
				return err
			}
			if originalPreRun != nil {
				return originalPreRun(cmd, args)
			}
			return nil
		}
		return cmd
	},
		manager.Metadata{
			SchemaVersion: "0.1.0",
			Vendor:        "Docker Inc.",
			Version:       "0.1",
			HookCommands:  []string{"build"},  // declare intent to hook into `build` subcommand invocations
		})
}

func RootCommand(dockerCli command.Cli) *cobra.Command {
	rootCmd := &cobra.Command{
		Short:            "Docker Hints",
		Use:              "hints",
		TraverseChildren: true,
	}

	rootCmd.AddCommand(hookCommand(dockerCli))
	return rootCmd
}

func hookCommand(dockerCli command.Cli) *cobra.Command {
	hookCmd := &cobra.Command{
		Use:   manager.HookSubcommandName,  // register hook command that the CLI will invoke
		Short: "runs the plugins hooks",
		RunE: utils.Adapt(func(ctx context.Context, args []string) error {
			runHooks(dockerCli.Out(), []byte(args[0]))
			return nil
		}),
		Args: cobra.ExactArgs(1),
	}

	return hookCmd
}

func runHooks(out io.Writer, input []byte) {
	var c manager.HookPluginData
	_ = json.Unmarshal(input, &c)

	hint := "Run this image with `docker run " + manager.TemplateReplaceFlagValue("tag") + "`" // template helper
	returnType := manager.HookMessage{
		Template: hint,
	}
	enc := json.NewEncoder(out)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "     ")
	_ = enc.Encode(returnType)
}

Which results in:
image


Performance Callouts

Edited: for current approach + benchmarks see #4376 (comment)

Security Callouts

The current approach works by letting plugins declare a list of strings which are the CLI commands (such as build, run, etc.) whose execution the plugin wants to be hooked into. The CLI then only invokes each plugin for the commands the plugin has declared support for.

Since a plugin might want to be invoked for more than one command, some information must be passed into the plugin when it is invoked so that it can decide what information it wants to add – which for right now, is codified into the HookPluginData type and contains only a string representing the command being executed. Richer output is made possible by employing a templating approach, wherein the plugin can return a template string with special values which the CLI then processes – e.g.

$ docker build -t my-image .

# a plugin returning
You can run the image you just built with `docker run {docker-cli-plugin-hook-flag-tag}`

# gets parsed into
You can run the image you just built with `docker run my-image`

This allows for plugins to provide useful/contextual messages, without passing any potentially sensitive information to any other binary.

Possible future developments to make this more secure are things such as always being explicit when a plugin is invoked, so that the user is aware, or prompting the user whether they are okay with a plugin hooking into a command execution on first execution.


- A picture of a cute animal (not mandatory but encouraged)

image

Release Notes:

Added plugin "hooks" which can be enabled (if the plugin supports it) via the config file.

@laurazard laurazard changed the title [DRAFT] Introduce support for CLI-plugin hooks 🚧 Introduce support for CLI plugin hooks Jun 26, 2023
@laurazard laurazard force-pushed the plugin-hooks branch 2 times, most recently from bee2b62 to 06a6617 Compare July 22, 2023 23:19
@codecov-commenter
Copy link

codecov-commenter commented Jul 22, 2023

Codecov Report

Merging #4376 (39b6c40) into master (ba64608) will increase coverage by 61.53%.
Report is 47 commits behind head on master.
The diff coverage is 31.15%.

Additional details and impacted files
@@             Coverage Diff             @@
##           master    #4376       +/-   ##
===========================================
+ Coverage        0   61.53%   +61.53%     
===========================================
  Files           0      292      +292     
  Lines           0    20378    +20378     
===========================================
+ Hits            0    12539    +12539     
- Misses          0     6945     +6945     
- Partials        0      894      +894     

@laurazard laurazard force-pushed the plugin-hooks branch 2 times, most recently from 8950476 to ac19075 Compare July 24, 2023 16:06
@laurazard laurazard marked this pull request as ready for review July 24, 2023 18:13
@laurazard laurazard requested a review from thaJeztah July 24, 2023 18:13
@laurazard laurazard changed the title 🚧 Introduce support for CLI plugin hooks Introduce support for CLI plugin hooks Aug 1, 2023
@laurazard laurazard requested review from rumpl and vvoland August 1, 2023 15:36
cmd/docker/docker.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cmd/docker/docker.go Outdated Show resolved Hide resolved
cmd/docker/docker.go Outdated Show resolved Hide resolved
cmd/docker/docker.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved

fmt.Fprintln(out, aec.LightBlueF.Apply("\nNext steps:"))
for _, m := range messages {
fmt.Fprintf(out, " - %s\n", m)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like "list" layout is enforced here. Should we also make sure that each message is 1-line only?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure about this one. I think it would be fine to have a multiline list type of hints, e.g.:

Next steps:
    - Keep in mind that lorem ipsum dolor sit amet.
(also, pellentesque ac eleifend tellus, ac malesuada dolor)
    - Nullam luctus rutrum fringilla

What does everyone think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could format each line of a message (after the first) so they at least start with the same indentaton? (a small terminal would wrap the lines, so in that case it'd look ugly anyway)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could format each line of a message (after the first) so they at least start with the same indentaton?

Next steps:
    - Keep in mind that lorem ipsum dolor sit amet.
      (also, pellentesque ac eleifend tellus, ac malesuada dolor)
    - Nullam luctus rutrum fringilla

Like this? Not sure if this is clearer or not for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! I think it looks much better (but that might just be a strong bias on my side 😅)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today in docker scout we don't display them as list items, even if there's multiple items:
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a jab at it – I like the idea, but slightly concerned about totally breaking formatting with the indentation in smaller terminals.

cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/metadata.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
@laurazard laurazard requested a review from Benehiko March 8, 2024 13:48
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/plugin.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
@laurazard laurazard force-pushed the plugin-hooks branch 2 times, most recently from fb02226 to 943d2f5 Compare March 11, 2024 12:56
@tonistiigi
Copy link
Member

script --return --quiet -c "docker -v" /dev/null ran

docker -v does not call into this code-path. Your latest comment uses docker version that does.

Still, I can't replicate the 8 seconds that you ran into. In my environment:

This is from codespaces and can be replicated there. Just make sure to install desktop to get the plugins bundle as only compose&buildx are installed by default.

8 seconds is indeed unexpected and it doesn't fit linear scaling from plugins count(considering rc2 baseline takes 300ms on same mode), but I think any double-digit percentage (=measurable) performance regression should be avoided.

I added some debug on that drop_cache run. Clearly still coming from that function. I think previously I had one extra plugin leftover and that causes the 8s -> 6s difference.

03:41:47.268036 > RunPluginHooks
03:41:47.284216 < listPluginCandidates done 
03:41:47.284630 > newPlugin /usr/lib/docker/cli-plugins/docker-scout
03:41:47.284628 > newPlugin /usr/lib/docker/cli-plugins/docker-extension
03:41:47.284761 > newPlugin /usr/lib/docker/cli-plugins/docker-feedback
03:41:47.284873 > newPlugin /usr/lib/docker/cli-plugins/docker-init
03:41:47.284909 > newPlugin /usr/lib/docker/cli-plugins/docker-compose
03:41:47.284935 > newPlugin /usr/lib/docker/cli-plugins/docker-dev
03:41:47.285002 > newPlugin /usr/lib/docker/cli-plugins/docker-sbom
03:41:47.285086 > newPlugin /usr/lib/docker/cli-plugins/docker-buildx
03:41:47.285099 > newPlugin /usr/lib/docker/cli-plugins/docker-debug
03:41:51.179446 < newPlugin /usr/lib/docker/cli-plugins/docker-feedback done
03:41:51.300104 < newPlugin /usr/lib/docker/cli-plugins/docker-dev done
03:41:51.357775 < newPlugin /usr/lib/docker/cli-plugins/docker-init done
03:41:51.401657 < newPlugin /usr/lib/docker/cli-plugins/docker-extension done
03:41:51.820787 < newPlugin /usr/lib/docker/cli-plugins/docker-debug done
03:41:52.002857 < newPlugin /usr/lib/docker/cli-plugins/docker-sbom done
03:41:52.955011 < newPlugin /usr/lib/docker/cli-plugins/docker-buildx done
03:41:52.959200 < newPlugin /usr/lib/docker/cli-plugins/docker-compose done
03:41:52.963600 < newPlugin /usr/lib/docker/cli-plugins/docker-scout done
03:41:52.963630 > ListPlugins done
03:41:52.963644 < RunPluginHooks 5.695607869s

real    0m6.049s
user    0m0.214s
sys     0m0.222s

Copy link
Member

@Benehiko Benehiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a lot better! LGTM

Comment on lines 14 to 17
fmt.Fprintln(out, aec.LightBlueF.Apply("\nNext steps:"))
for _, n := range messages {
_, _ = fmt.Fprintf(out, " - %s\n", n)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current implementation of hooks looks like this:
image

And with multiple messages:
image

Basically two main differences:

  • Next steps: vs What's Next? and colors
  • - in front of each lines

I can change the scout CLI to add - on each line, so we ensure it's done the same way everywhere.
About the title I wonder if What's next? isn't a bit better. But I'll defer to @jalonsogo

@laurazard
Copy link
Member Author

laurazard commented Mar 18, 2024

Replying here to provide a coherent response to the concerns brought up by @tonistiigi:

To properly test performance, I made the following changes to cmd/docker/docker.go:

diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go
index 431b1e027..ca9142151 100644
--- a/cmd/docker/docker.go
+++ b/cmd/docker/docker.go
@@ -306,7 +306,7 @@ func runDocker(dockerCli *command.DockerCli) error {
                }
        }

-       var subCommand *cobra.Command
+       // var subCommand *cobra.Command
        if len(args) > 0 {
                ccmd, _, err := cmd.Find(args)
                if err != nil || pluginmanager.IsPluginCommand(ccmd) {
@@ -318,7 +318,7 @@ func runDocker(dockerCli *command.DockerCli) error {
                        // cmd.Execute() which deals with reporting
                        // "command not found" in a consistent way.
                } else {
-                       subCommand = ccmd
+                       // subCommand = ccmd
                }
        }

@@ -332,9 +332,9 @@ func runDocker(dockerCli *command.DockerCli) error {

        // If the command is being executed in an interactive terminal,
        // run the plugin hooks (but don't throw an error if something misbehaves)
-       if dockerCli.Out().IsTerminal() && subCommand != nil {
-               _ = pluginmanager.RunPluginHooks(dockerCli, cmd, subCommand)
-       }
+       // if dockerCli.Out().IsTerminal() && subCommand != nil {
         _ = pluginmanager.RunPluginHooks(dockerCli, cmd, subCommand)
+       // }

        return nil
 }

And built a version, with all the same code, with and without hooks. After that, I benchmarked with:

hyperfine --prepare 'sudo sh -c "sync; echo 1 > /proc/sys/vm/drop_caches"' './build/docker version' './build/docker-with-hooks version'

So, lets test this:

This was with the plugins bundle that ships with Docker Desktop (9) as it clearly depends on the number of plugins in the discovery directories.

We'll test with a number of different plugins:

  buildx: Docker Buildx (Docker Inc.)
  compose: Docker Compose (Docker Inc.)
  feedback: Provide feedback, right in your terminal! (Docker Inc.)
  hints: Docker Hints (Docker Inc.)
  long: Docker Long Running Example Plugin (Docker Inc.)

Benchmark 1: ./build/docker version
  Time (mean ± σ):      28.9 ms ±   7.2 ms    [User: 3.5 ms, System: 11.9 ms]
  Range (minmax):    25.5 ms61.1 ms    23 runs

Benchmark 2: ./build/docker-with-hooks version
  Time (mean ± σ):      66.8 ms ±   3.6 ms    [User: 32.8 ms, System: 65.8 ms]
  Range (minmax):    64.1 ms78.1 ms    28 runs

Summary
  ./build/docker version ran
    2.31 ± 0.59 times faster than ./build/docker-with-hooks version

So that's ~40ms difference. With more plugins:

  buildx: Docker Buildx (Docker Inc.)
  compose: Docker Compose (Docker Inc.)
  dev: Docker Dev Environments (Docker Inc.)
  feedback: Provide feedback, right in your terminal! (Docker Inc.)
  hints: Docker Hints (Docker Inc.)
  init: Creates Docker-related starter files for your project (Docker Inc.)
  long: Docker Long Running Example Plugin (Docker Inc.)


Benchmark 1: ./build/docker version
  Time (mean ± σ):      30.0 ms ±   5.8 ms    [User: 4.4 ms, System: 11.7 ms]
  Range (minmax):    24.8 ms52.3 ms    21 runs

Benchmark 2: ./build/docker-with-hooks version
  Time (mean ± σ):      71.9 ms ±   8.1 ms    [User: 38.3 ms, System: 83.8 ms]
  Range (minmax):    66.6 ms103.4 ms    22 runs

  Warning: The first benchmarking run for this command was significantly slower than the rest (103.4 ms). This could be caused by (filesystem) caches that were not filled until after the first run. You are already using the '--prepare' option which can be used to clear caches. If you did not use a cache-clearing command with '--prepare', you can either try that or consider using the '--warmup' option to fill those caches before the actual benchmark.

Summary
  ./build/docker version ran
    2.40 ± 0.54 times faster than ./build/docker-with-hooks version

Huh that's about 40ms too. Lets try more:

  buildx: Docker Buildx (Docker Inc.)
  compose: Docker Compose (Docker Inc.)
  dev: Docker Dev Environments (Docker Inc.)
  feedback: Provide feedback, right in your terminal! (Docker Inc.)
  hints: Docker Hints (Docker Inc.)
  init: Creates Docker-related starter files for your project (Docker Inc.)
  long: Docker Long Running Example Plugin (Docker Inc.)
  sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc.)


Benchmark 1: ./build/docker version
  Time (mean ± σ):      27.1 ms ±   1.8 ms    [User: 4.1 ms, System: 10.9 ms]
  Range (minmax):    25.2 ms35.7 ms    30 runs

Benchmark 2: ./build/docker-with-hooks version
  Time (mean ± σ):      71.9 ms ±   4.2 ms    [User: 45.8 ms, System: 90.9 ms]
  Range (minmax):    66.4 ms85.0 ms    31 runs

Summary
  ./build/docker version ran
    2.66 ± 0.24 times faster than ./build/docker-with-hooks version

It's still about the same. It seems like the performance penalty does not depend on the number of plugins, as we thought (from the original PR text):

or more specifically, this will always be the same as the slowest CLI plugin.

Adding scout:

  buildx: Docker Buildx (Docker Inc.)
  compose: Docker Compose (Docker Inc.)
  debug: Get a shell into any image or container. (Docker Inc.)
  dev: Docker Dev Environments (Docker Inc.)
  feedback: Provide feedback, right in your terminal! (Docker Inc.)
  hints: Docker Hints (Docker Inc.)
  init: Creates Docker-related starter files for your project (Docker Inc.)
  long: Docker Long Running Example Plugin (Docker Inc.)
  sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc.)
  scout: Docker Scout (Docker Inc.)


Benchmark 1: ./build/docker version
  Time (mean ± σ):      27.3 ms ±   1.9 ms    [User: 3.8 ms, System: 11.0 ms]
  Range (min … max):    25.5 ms …  36.0 ms    26 runs

Benchmark 2: ./build/docker-with-hooks version
  Time (mean ± σ):      88.6 ms ±  10.6 ms    [User: 72.4 ms, System: 137.5 ms]
  Range (min … max):    81.0 ms … 134.6 ms    23 runs

Summary
  ./build/docker version ran
    3.25 ± 0.45 times faster than ./build/docker-with-hooks version

That's 60ms, 20ms slower than without. I guess that's because

~/.docker/cli-plugins/docker-scout docker-cli-plugin-metadata  0.05s user 0.01s system 116% cpu 0.058 total

whereas other plugins are a good bit faster

~/.docker/cli-plugins/docker-buildx  0.03s user 0.01s system 104% cpu 0.039 total
~/.docker/cli-plugins/docker-compose  0.02s user 0.02s system 110% cpu 0.037 total
~/.docker/cli-plugins/docker-sbom  0.01s user 0.02s system 121% cpu 0.022 total

This checks out with what we expect.

Currently, the performance penalty is as we expect, barring any strange edge-cases. As called out in the PR description, a mitigation (that we're planning on working on after merging this) for this issue is to cache plugin metadata so that we don't always have to execute every plugin to see if they support hooks.

I'll also be adding a very naive timeout to the plugin scanning logic, to make sure we cap that out and never let it go longer than 80 or 100ms

@tonistiigi
Copy link
Member

It's still about the same. It seems like the performance penalty does not depend on the number of plugins, as we thought (from the original PR text):

You can see in your output how user/system gradually grows with every added plugin, spinning the CPU more each time. In the last example, the resource usage is 14x of baseline, 9x before scout is added.

As these plugins all run in their own goroutine, the real time depends on cpu count, OS's exec implementation(including malware detection mechanisms), prewarmed cache, system load, and other factors.

Docker is run by millions of users, with various os/arch/perf configurations. Afaik Desktop does not come with a free 10+ core machine. If you compare absolute numbers then obviously users with low perf and low cpu count would see the impact more (and CLI is released to some really low-powered machines) than machines with a lot of resources to spare. To compare the absolute numbers, you would need a big test matrix of all the OS(maybe even kernels & infra providers), different ranges of cpu counts and perf generations, memory pressure, invocation frequency etc. But we don't need to compare the absolute numbers, just that previously we needed resources to run 1 binary and now we need resources to run 10 (and more with any future update or extra plugin the user has added). Unless you can defy some laws of physics, the latter will be slower for the users.

Adding scout:

Scout binary is more than 4x bigger than dev/debug/init/feedback etc small plugins. Minimally, the extra resources need to be spent to load these binaries into memory, but it is not impossible that there is some init() rule that can be optimized as well(and other bottlenecks for spawning binaries in parallel are likely OS implementation-specific). Compared to buildx, what is only slightly smaller, scout's metadata cmd takes 25ms vs 15ms. How much impact a bigger binary or init() function has when loading a big bundle of binaries may not be universal in different configurations. This does show another weakness of this approach though - should any other binary make a mistake or just be unnecessarily bloated, it slows down the whole docker experience for any command.

Currently, the performance penalty is as we expect,

That's another position where we differ. To me, any measurable performance regression from spinning the CPU needlessly should be avoided. Comparing this to other historic regressions I've either fixed or reported in the past, I think this is the biggest one yet (at least if the high multi-core machines are excluded).

@laurazard
Copy link
Member Author

laurazard commented Mar 19, 2024

This (current) performance discussion is moot as we'll be making some implementation changes that allow us to not have to scan/execute all the plugins, but for discussion's sake:

You can see in your output how user/system gradually grows with every added plugin

Yes, obviously cpu time scales with # plugins. I'd expected that we were discussing real time before, esp. since we're talking about the Desktop plugin bundles that most people would be running on their main development computers, and not in CI/etc. Otherwise if we're discussing CI environments why are you testing with more than 2 plugins? We can discuss either case, but discussing the worst case scenario of "extremely restricted environment but for some reason with Docker Desktop + all the plugins bundled installed" doesn't seem very productive.

Docker is run by millions of users, with various os/arch/perf configurations. Afaik Desktop does not come with a free 10+ core machine. If you compare absolute numbers then obviously users with low perf and low cpu count would see the impact more (and CLI is released to some really low-powered machines) than machines with a lot of resources to spare.

Yes, it is. That still does not mean that we cannot incur a performance hit ever, especially when this is done as a step in the direction of overall improving performance.

This does show another weakness of this approach though - should any other binary make a mistake or just be unnecessarily bloated, it slows down the whole docker experience for any command.

Maybe I wasn't clear in my previous reply, but this sentence – I'll also be adding a very naive timeout to the plugin scanning logic, to make sure we cap that out and never let it go longer than 80 or 100ms – refers to the plugin execution time as well.

To me, any measurable performance regression from spinning the CPU needlessly should be avoided

I agree. Which is why we're implementing this – so we can remove the wrapper and the "needless cpu spinning" that it does.


Regardless, I'll amend the PR soon – I was discussing with @thaJeztah and we were thinking about instead storing whether a plugin supports hooks in the CLI's config file, which would negate the need to cycle over/execute all the plugins to check for support.

@laurazard
Copy link
Member Author

laurazard commented Mar 20, 2024

Pushed a change reworking the plugin hook listing+scanning logic – instead of scanning the plugin folders and executing every candidate to check their metadata for hook support, we now expect plugins with hooks to be configured in the CLI's config.json. For example:

{
  "auths": {},
  "plugins": {
    "hints": {
      "hooks": "pull,build"
    }
  }
}

The new logic looks only for plugins configured this way, which significantly reduces execution time (and makes it so that performance does not get worse with the amount of plugins bundled).

Applying the above patch (executes plugins even if not attached to a tty):

Benchmark 1: ./build/docker version
  Time (mean ± σ):       9.3 ms ±   1.0 ms    [User: 3.7 ms, System: 3.1 ms]
  Range (minmax):     6.8 ms13.7 ms    211 runs

Benchmark 2: ./build/docker-without-hooks version
  Time (mean ± σ):       9.4 ms ±   2.0 ms    [User: 3.6 ms, System: 3.2 ms]
  Range (minmax):     6.8 ms40.6 ms    288 runs

Summary
  ./build/docker version ran
    1.01 ± 0.24 times faster than ./build/docker-without-hooks version

That is for docker version, which no plugins register hook from. Measuring w/ a plugin enabled – with the example plugin, and instrumenting RunPluginHooks to time it's execution:

./build/docker pull alpine
Using default tag: latest
latest: Pulling from library/alpine
Digest: sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
Status: Image is up to date for alpine:latest
docker.io/library/alpine:latest

Next steps:
    - Run this image with `docker run alpine`
RunPluginHooks took 12.409638ms
...
RunPluginHooks took 8.642533ms
...
RunPluginHooks took 14.853688ms

from end to end, including calling the hook plugin, which is a lot more reasonable.

Still want to do a quick run through of some codebases to check no plugin is going to break if we do this.

Copy link
Member

@Benehiko Benehiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

cli-plugins/manager/hooks.go Outdated Show resolved Hide resolved
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
@neersighted neersighted merged commit 799bf52 into docker:master Mar 22, 2024
88 checks passed
@neersighted neersighted deleted the plugin-hooks branch March 22, 2024 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants